diff --git a/README.md b/README.md index 43b112b..204d7d1 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Central hub for activity, tasks, goals, and tools. Built for TopDogLabs to track - High priority and overdue task counts - Recently updated and completed tasks - Quick actions to Gantt Board +- Quick-action task links target `gantt-board /tasks` with `scope=active-sprint` ### Projects - Active sprint tracking (global sprints from gantt-board) diff --git a/app/api/voxyz/daily-missions/route.ts b/app/api/voxyz/daily-missions/route.ts new file mode 100644 index 0000000..fc0092e --- /dev/null +++ b/app/api/voxyz/daily-missions/route.ts @@ -0,0 +1,151 @@ +/** + * Voxyz API Routes - Daily Missions + * Handles daily mission CRUD operations and generation + */ + +import { NextRequest, NextResponse } from 'next/server'; +import { + generateDailyMissions, + getTodaysDailyMission, + approveDailyMission, + scheduleDailyMission, + completeDailyMission, + runDailyMissionGeneration +} from '@/lib/services/daily-mission-agent'; +import { getProposalsForDate, approveProposal, rejectProposal } from '@/lib/services/proposal.service'; +import { createClient } from '@/lib/supabase/server'; + +// GET /api/voxyz/daily-missions - Get today's daily mission +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const date = searchParams.get('date'); + + if (date) { + // Get proposals for specific date + const proposals = await getProposalsForDate(new Date(date)); + return NextResponse.json({ proposals }); + } + + // Get today's daily mission + const dailyMission = await getTodaysDailyMission(); + + if (!dailyMission) { + return NextResponse.json({ + dailyMission: null, + message: 'No daily mission generated yet' + }); + } + + // Fetch full proposal details + const supabase = await createClient(); + const proposalIds = [ + dailyMission.primary_mission_id, + dailyMission.secondary_mission_id, + dailyMission.tertiary_mission_id, + ].filter(Boolean); + + const { data: proposals } = await supabase + .from('ops_mission_proposals') + .select('*') + .in('id', proposalIds); + + return NextResponse.json({ + dailyMission, + proposals: proposals || [], + }); + + } catch (error) { + console.error('Error fetching daily mission:', error); + return NextResponse.json( + { error: 'Failed to fetch daily mission' }, + { status: 500 } + ); + } +} + +// POST /api/voxyz/daily-missions - Generate daily missions +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { date, force = false } = body; + + const targetDate = date ? new Date(date) : new Date(); + + const result = await generateDailyMissions({ + date: targetDate, + targetMissionCount: 3, + includeReactions: true, + forceRegenerate: force, + }); + + if (!result) { + return NextResponse.json( + { error: 'Failed to generate daily missions' }, + { status: 500 } + ); + } + + return NextResponse.json(result, { status: 201 }); + + } catch (error) { + console.error('Error generating daily missions:', error); + return NextResponse.json( + { error: 'Failed to generate daily missions' }, + { status: 500 } + ); + } +} + +// PATCH /api/voxyz/daily-missions - Update daily mission status +export async function PATCH(request: NextRequest) { + try { + const body = await request.json(); + const { id, action, rating, notes } = body; + + if (!id || !action) { + return NextResponse.json( + { error: 'Missing required fields: id, action' }, + { status: 400 } + ); + } + + let success = false; + + switch (action) { + case 'approve': + success = await approveDailyMission(id); + break; + + case 'schedule': + success = await scheduleDailyMission(id); + break; + + case 'complete': + success = await completeDailyMission(id, rating, notes); + break; + + default: + return NextResponse.json( + { error: `Unknown action: ${action}` }, + { status: 400 } + ); + } + + if (!success) { + return NextResponse.json( + { error: `Failed to ${action} daily mission` }, + { status: 500 } + ); + } + + return NextResponse.json({ success: true }); + + } catch (error) { + console.error('Error updating daily mission:', error); + return NextResponse.json( + { error: 'Failed to update daily mission' }, + { status: 500 } + ); + } +} diff --git a/app/api/voxyz/proposals/route.ts b/app/api/voxyz/proposals/route.ts new file mode 100644 index 0000000..d203cb2 --- /dev/null +++ b/app/api/voxyz/proposals/route.ts @@ -0,0 +1,189 @@ +/** + * Voxyz API Routes - Mission Proposals + * Handles proposal CRUD operations + */ + +import { NextRequest, NextResponse } from 'next/server'; +import { + getProposalsForDate, + approveProposal, + rejectProposal, + completeProposal, + getTopProposalsForToday, + expireOldProposals +} from '@/lib/services/proposal.service'; +import { createClient } from '@/lib/supabase/server'; + +// GET /api/voxyz/proposals - Get proposals +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const date = searchParams.get('date'); + const today = searchParams.get('today'); + const status = searchParams.get('status'); + + if (today) { + const proposals = await getTopProposalsForToday(); + return NextResponse.json({ proposals }); + } + + if (date) { + const proposals = await getProposalsForDate(new Date(date)); + return NextResponse.json({ proposals }); + } + + // Get all proposals with optional status filter + const supabase = await createClient(); + let query = supabase.from('ops_mission_proposals').select('*'); + + if (status) { + query = query.eq('status', status); + } + + const { data: proposals, error } = await query + .order('created_at', { ascending: false }) + .limit(100); + + if (error) { + throw error; + } + + return NextResponse.json({ proposals: proposals || [] }); + + } catch (error) { + console.error('Error fetching proposals:', error); + return NextResponse.json( + { error: 'Failed to fetch proposals' }, + { status: 500 } + ); + } +} + +// POST /api/voxyz/proposals - Create a new proposal (manual) +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { title, description, taskIds, projectIds, priorityScore } = body; + + if (!title) { + return NextResponse.json( + { error: 'Missing required field: title' }, + { status: 400 } + ); + } + + const supabase = await createClient(); + + const proposal = { + proposal_date: new Date().toISOString().split('T')[0], + status: 'pending', + priority_score: priorityScore || 50, + passes_cap_gates: true, + cap_gate_violations: [], + source_task_ids: taskIds || [], + source_project_ids: projectIds || [], + title, + description: description || null, + rationale: 'Manually created proposal', + ai_analysis: {}, + expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + }; + + const { data, error } = await supabase + .from('ops_mission_proposals') + .insert(proposal) + .select() + .single(); + + if (error) { + throw error; + } + + return NextResponse.json({ proposal: data }, { status: 201 }); + + } catch (error) { + console.error('Error creating proposal:', error); + return NextResponse.json( + { error: 'Failed to create proposal' }, + { status: 500 } + ); + } +} + +// PATCH /api/voxyz/proposals - Update proposal status +export async function PATCH(request: NextRequest) { + try { + const body = await request.json(); + const { id, action, reason, notes } = body; + + if (!id || !action) { + return NextResponse.json( + { error: 'Missing required fields: id, action' }, + { status: 400 } + ); + } + + let success = false; + + switch (action) { + case 'approve': + success = await approveProposal(id, 'manual-user'); // TODO: Get actual user ID + break; + + case 'reject': + if (!reason) { + return NextResponse.json( + { error: 'Rejection requires a reason' }, + { status: 400 } + ); + } + success = await rejectProposal(id, reason); + break; + + case 'complete': + success = await completeProposal(id, notes); + break; + + default: + return NextResponse.json( + { error: `Unknown action: ${action}` }, + { status: 400 } + ); + } + + if (!success) { + return NextResponse.json( + { error: `Failed to ${action} proposal` }, + { status: 500 } + ); + } + + return NextResponse.json({ success: true }); + + } catch (error) { + console.error('Error updating proposal:', error); + return NextResponse.json( + { error: 'Failed to update proposal' }, + { status: 500 } + ); + } +} + +// DELETE /api/voxyz/proposals - Expire old proposals +export async function DELETE(request: NextRequest) { + try { + const expiredCount = await expireOldProposals(); + + return NextResponse.json({ + success: true, + expiredCount + }); + + } catch (error) { + console.error('Error expiring proposals:', error); + return NextResponse.json( + { error: 'Failed to expire proposals' }, + { status: 500 } + ); + } +} diff --git a/app/projects/page.tsx b/app/projects/page.tsx index d05e3a7..71d95fc 100644 --- a/app/projects/page.tsx +++ b/app/projects/page.tsx @@ -572,26 +572,34 @@ export default async function ProjectsOverviewPage() { - + High Priority - View urgent tasks + Urgent/high in current sprint - + In Progress - Active tasks + Current sprint in-progress tasks diff --git a/lib/services/cap-gates.service.ts b/lib/services/cap-gates.service.ts new file mode 100644 index 0000000..4e088c3 --- /dev/null +++ b/lib/services/cap-gates.service.ts @@ -0,0 +1,577 @@ +/** + * Voxyz Cap Gates Service + * Queue management and capacity control system + * Phase 10: Autonomous Architecture + */ + +import { + CapGate, + CapGateCheck, + CapGateCheckResult, + CapGateViolation, + CapGateConfig, + MissionProposal, + DailyMission +} from '@/lib/supabase/voxyz.types'; +import { createClient } from '@/lib/supabase/client'; + +const DEFAULT_CONFIG: CapGateConfig = { + dailyMissionLimit: 3, + concurrentHighPriorityLimit: 2, + maxDailyWorkloadMinutes: 480, + minFocusTimeMinutes: 120, + minCalendarBufferMinutes: 15, + reactionRatio: 0.30, +}; + +/** + * Get all active cap gates from the database + */ +export async function getActiveCapGates(): Promise { + const supabase = createClient(); + + const { data, error } = await supabase + .from('ops_cap_gates') + .select('*') + .eq('is_active', true) + .order('gate_type', { ascending: true }); + + if (error) { + console.error('Error fetching cap gates:', error); + return []; + } + + return data || []; +} + +/** + * Get a specific cap gate by type + */ +export async function getCapGateByType(gateType: CapGate['gate_type']): Promise { + const supabase = createClient(); + + const { data, error } = await supabase + .from('ops_cap_gates') + .select('*') + .eq('gate_type', gateType) + .eq('is_active', true) + .single(); + + if (error) { + console.error('Error fetching cap gate:', error); + return null; + } + + return data; +} + +/** + * Update cap gate current value + */ +export async function updateCapGateValue( + gateId: string, + currentValue: number +): Promise { + const supabase = createClient(); + + const { error } = await supabase + .from('ops_cap_gates') + .update({ + current_value: currentValue, + last_evaluated_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }) + .eq('id', gateId); + + if (error) { + console.error('Error updating cap gate:', error); + return false; + } + + return true; +} + +/** + * Check all cap gates for a potential new proposal + */ +export async function checkCapGates( + proposedMissionMinutes: number, + existingProposals: MissionProposal[], + config: CapGateConfig = DEFAULT_CONFIG +): Promise { + const gates = await getActiveCapGates(); + const checks: CapGateCheck[] = []; + const violations: CapGateViolation[] = []; + + for (const gate of gates) { + const check = await evaluateCapGate(gate, proposedMissionMinutes, existingProposals, config); + checks.push(check); + + if (!check.passed && check.violation) { + violations.push(check.violation); + } + } + + // Always check these essential gates even if not in DB + const essentialChecks = await checkEssentialCapGates( + proposedMissionMinutes, + existingProposals, + config + ); + + checks.push(...essentialChecks.checks); + violations.push(...essentialChecks.violations); + + return { + allPassed: violations.length === 0, + checks, + violations, + summary: violations.length === 0 + ? 'All cap gates passed' + : `${violations.length} cap gate violation${violations.length !== 1 ? 's' : ''}`, + }; +} + +/** + * Evaluate a single cap gate + */ +async function evaluateCapGate( + gate: CapGate, + proposedMissionMinutes: number, + existingProposals: MissionProposal[], + config: CapGateConfig +): Promise { + let currentValue = gate.current_value; + let passed = true; + let violation: CapGateViolation | undefined; + + switch (gate.gate_type) { + case 'daily_mission_limit': + currentValue = existingProposals.filter(p => + p.status !== 'rejected' && p.status !== 'expired' + ).length; + passed = currentValue < (gate.max_value || config.dailyMissionLimit); + if (!passed) { + violation = { + gate_name: gate.name, + gate_type: gate.gate_type, + current_value: currentValue, + max_value: gate.max_value, + violation_message: `Daily mission limit of ${gate.max_value} reached (${currentValue} existing)`, + }; + } + break; + + case 'workload_capacity': + const totalMinutes = existingProposals.reduce((sum, p) => + sum + (p.estimated_duration_minutes || 120), 0 + ); + currentValue = totalMinutes + proposedMissionMinutes; + passed = currentValue <= (gate.max_value || config.maxDailyWorkloadMinutes); + if (!passed) { + violation = { + gate_name: gate.name, + gate_type: gate.gate_type, + current_value: currentValue, + max_value: gate.max_value, + violation_message: `Workload would exceed ${gate.max_value} minutes (${currentValue} proposed)`, + }; + } + break; + + case 'concurrent_high_priority': + const highPriorityCount = existingProposals.filter(p => + p.priority_score >= 75 && p.status !== 'rejected' + ).length; + currentValue = highPriorityCount; + passed = currentValue < (gate.max_value || config.concurrentHighPriorityLimit); + if (!passed) { + violation = { + gate_name: gate.name, + gate_type: gate.gate_type, + current_value: currentValue, + max_value: gate.max_value, + violation_message: `Too many concurrent high-priority missions (${currentValue} of ${gate.max_value} max)`, + }; + } + break; + + case 'focus_time_minimum': + // Calculate if we have enough focus time allocated + const totalAllocatedMinutes = existingProposals.reduce((sum, p) => + sum + (p.estimated_duration_minutes || 120), 0 + ); + currentValue = totalAllocatedMinutes; + // This is a minimum gate, so we check if we still have capacity + passed = true; // Focus time is a soft constraint + break; + + case 'reaction_ratio': + // Track reaction time allocation (30% of workday) + const workdayMinutes = 480; // 8 hours + const reactionMinutes = Math.round(workdayMinutes * (gate.max_value / 100 || config.reactionRatio)); + const plannedMinutes = existingProposals.reduce((sum, p) => + sum + (p.estimated_duration_minutes || 120), 0 + ); + currentValue = Math.round((plannedMinutes / workdayMinutes) * 100); + // Reaction ratio is tracked but not blocking + passed = true; + break; + + default: + passed = true; + } + + // Update gate value in database + await updateCapGateValue(gate.id, currentValue); + + return { + gate, + passed, + currentValue, + violation, + }; +} + +/** + * Check essential cap gates that should always be evaluated + */ +async function checkEssentialCapGates( + proposedMissionMinutes: number, + existingProposals: MissionProposal[], + config: CapGateConfig +): Promise { + const checks: CapGateCheck[] = []; + const violations: CapGateViolation[] = []; + const now = new Date(); + + // Gate 1: Daily mission limit + const activeProposals = existingProposals.filter(p => + p.status !== 'rejected' && p.status !== 'expired' + ); + const dailyLimitCheck: CapGateCheck = { + gate: { + id: 'essential-daily-limit', + name: 'Daily Mission Limit', + description: 'Maximum missions per day', + gate_type: 'daily_mission_limit', + max_value: config.dailyMissionLimit, + min_value: 0, + unit: 'count', + current_value: activeProposals.length, + last_evaluated_at: now.toISOString(), + policy_id: null, + is_active: true, + is_blocking: false, + evaluation_window_hours: 24, + created_at: now.toISOString(), + updated_at: now.toISOString(), + created_by: null, + updated_by: null, + }, + passed: activeProposals.length < config.dailyMissionLimit, + currentValue: activeProposals.length, + }; + + if (!dailyLimitCheck.passed) { + dailyLimitCheck.gate.is_blocking = true; + violations.push({ + gate_name: dailyLimitCheck.gate.name, + gate_type: dailyLimitCheck.gate.gate_type, + current_value: dailyLimitCheck.currentValue, + max_value: config.dailyMissionLimit, + violation_message: `Daily mission limit of ${config.dailyMissionLimit} reached`, + }); + } + checks.push(dailyLimitCheck); + + // Gate 2: Workload capacity + const totalWorkload = activeProposals.reduce((sum, p) => + sum + (p.estimated_duration_minutes || 120), 0 + ); + const proposedWorkload = totalWorkload + proposedMissionMinutes; + const workloadCheck: CapGateCheck = { + gate: { + id: 'essential-workload', + name: 'Workload Capacity', + description: 'Daily workload capacity', + gate_type: 'workload_capacity', + max_value: config.maxDailyWorkloadMinutes, + min_value: 0, + unit: 'minutes', + current_value: proposedWorkload, + last_evaluated_at: now.toISOString(), + policy_id: null, + is_active: true, + is_blocking: false, + evaluation_window_hours: 24, + created_at: now.toISOString(), + updated_at: now.toISOString(), + created_by: null, + updated_by: null, + }, + passed: proposedWorkload <= config.maxDailyWorkloadMinutes, + currentValue: proposedWorkload, + }; + + if (!workloadCheck.passed) { + workloadCheck.gate.is_blocking = true; + violations.push({ + gate_name: workloadCheck.gate.name, + gate_type: workloadCheck.gate.gate_type, + current_value: proposedWorkload, + max_value: config.maxDailyWorkloadMinutes, + violation_message: `Workload would exceed ${config.maxDailyWorkloadMinutes} minutes (${proposedWorkload} proposed)`, + }); + } + checks.push(workloadCheck); + + // Gate 3: Concurrent high priority + const highPriorityCount = activeProposals.filter(p => p.priority_score >= 75).length; + const highPriorityCheck: CapGateCheck = { + gate: { + id: 'essential-high-priority', + name: 'Concurrent High Priority', + description: 'Limit on concurrent high-priority work', + gate_type: 'concurrent_high_priority', + max_value: config.concurrentHighPriorityLimit, + min_value: 0, + unit: 'count', + current_value: highPriorityCount, + last_evaluated_at: now.toISOString(), + policy_id: null, + is_active: true, + is_blocking: false, + evaluation_window_hours: 24, + created_at: now.toISOString(), + updated_at: now.toISOString(), + created_by: null, + updated_by: null, + }, + passed: highPriorityCount < config.concurrentHighPriorityLimit, + currentValue: highPriorityCount, + }; + + if (!highPriorityCheck.passed) { + highPriorityCheck.gate.is_blocking = true; + violations.push({ + gate_name: highPriorityCheck.gate.name, + gate_type: highPriorityCheck.gate.gate_type, + current_value: highPriorityCount, + max_value: config.concurrentHighPriorityLimit, + violation_message: `Too many concurrent high-priority missions`, + }); + } + checks.push(highPriorityCheck); + + return { + allPassed: violations.length === 0, + checks, + violations, + summary: violations.length === 0 + ? 'All essential cap gates passed' + : `${violations.length} essential cap gate violation${violations.length !== 1 ? 's' : ''}`, + }; +} + +/** + * Get current capacity status + */ +export async function getCapacityStatus(config: CapGateConfig = DEFAULT_CONFIG): Promise<{ + dailyMissionsUsed: number; + dailyMissionsRemaining: number; + workloadUsedMinutes: number; + workloadRemainingMinutes: number; + highPriorityUsed: number; + highPriorityRemaining: number; + focusTimeAllocatedMinutes: number; + reactionTimeReservedMinutes: number; +}> { + const supabase = createClient(); + const today = new Date().toISOString().split('T')[0]; + + // Get today's proposals + const { data: proposals } = await supabase + .from('ops_mission_proposals') + .select('*') + .eq('proposal_date', today) + .neq('status', 'rejected') + .neq('status', 'expired'); + + const activeProposals = proposals || []; + + const dailyMissionsUsed = activeProposals.length; + const workloadUsedMinutes = activeProposals.reduce((sum, p) => + sum + (p.estimated_duration_minutes || 120), 0 + ); + const highPriorityUsed = activeProposals.filter(p => p.priority_score >= 75).length; + + // Calculate focus time (non-reaction time) + const workdayMinutes = 480; + const reactionMinutes = Math.round(workdayMinutes * config.reactionRatio); + const focusTimeAllocatedMinutes = Math.min(workloadUsedMinutes, workdayMinutes - reactionMinutes); + + return { + dailyMissionsUsed, + dailyMissionsRemaining: Math.max(0, config.dailyMissionLimit - dailyMissionsUsed), + workloadUsedMinutes, + workloadRemainingMinutes: Math.max(0, config.maxDailyWorkloadMinutes - workloadUsedMinutes), + highPriorityUsed, + highPriorityRemaining: Math.max(0, config.concurrentHighPriorityLimit - highPriorityUsed), + focusTimeAllocatedMinutes, + reactionTimeReservedMinutes: reactionMinutes, + }; +} + +/** + * Check if a new proposal can be added + */ +export async function canAddProposal( + estimatedMinutes: number = 120 +): Promise<{ allowed: boolean; reason?: string }> { + const status = await getCapacityStatus(); + + if (status.dailyMissionsRemaining <= 0) { + return { allowed: false, reason: 'Daily mission limit reached' }; + } + + if (status.workloadRemainingMinutes < estimatedMinutes) { + return { allowed: false, reason: 'Insufficient workload capacity' }; + } + + return { allowed: true }; +} + +/** + * Create default cap gates in database + */ +export async function initializeCapGates(config: CapGateConfig = DEFAULT_CONFIG): Promise { + const supabase = createClient(); + + // Check if cap gates already exist + const { count } = await supabase + .from('ops_cap_gates') + .select('*', { count: 'exact', head: true }); + + if (count && count > 0) { + return true; // Already initialized + } + + // Create default cap gates + const defaultGates: Omit[] = [ + { + name: 'Daily Mission Limit', + description: 'Maximum number of missions per day', + gate_type: 'daily_mission_limit', + max_value: config.dailyMissionLimit, + min_value: 1, + unit: 'count', + current_value: 0, + last_evaluated_at: null, + policy_id: null, + is_active: true, + is_blocking: true, + evaluation_window_hours: 24, + created_by: null, + updated_by: null, + }, + { + name: 'Workload Capacity', + description: 'Maximum daily workload in minutes', + gate_type: 'workload_capacity', + max_value: config.maxDailyWorkloadMinutes, + min_value: 60, + unit: 'minutes', + current_value: 0, + last_evaluated_at: null, + policy_id: null, + is_active: true, + is_blocking: true, + evaluation_window_hours: 24, + created_by: null, + updated_by: null, + }, + { + name: 'Concurrent High Priority', + description: 'Maximum concurrent high-priority missions', + gate_type: 'concurrent_high_priority', + max_value: config.concurrentHighPriorityLimit, + min_value: 0, + unit: 'count', + current_value: 0, + last_evaluated_at: null, + policy_id: null, + is_active: true, + is_blocking: true, + evaluation_window_hours: 24, + created_by: null, + updated_by: null, + }, + { + name: 'Focus Time Minimum', + description: 'Minimum focus time required per day', + gate_type: 'focus_time_minimum', + max_value: 480, + min_value: config.minFocusTimeMinutes, + unit: 'minutes', + current_value: 0, + last_evaluated_at: null, + policy_id: null, + is_active: true, + is_blocking: false, + evaluation_window_hours: 24, + created_by: null, + updated_by: null, + }, + { + name: 'Reaction Time Reserve', + description: 'Time reserved for spontaneous reactions', + gate_type: 'reaction_ratio', + max_value: Math.round(config.reactionRatio * 100), + min_value: 10, + unit: 'percentage', + current_value: 0, + last_evaluated_at: null, + policy_id: null, + is_active: true, + is_blocking: false, + evaluation_window_hours: 24, + created_by: null, + updated_by: null, + }, + ]; + + const { error } = await supabase + .from('ops_cap_gates') + .insert(defaultGates); + + if (error) { + console.error('Error initializing cap gates:', error); + return false; + } + + return true; +} + +/** + * Reset cap gate values for a new day + */ +export async function resetCapGatesForNewDay(): Promise { + const supabase = createClient(); + + const { error } = await supabase + .from('ops_cap_gates') + .update({ + current_value: 0, + last_evaluated_at: new Date().toISOString(), + is_blocking: false, + }) + .eq('is_active', true); + + if (error) { + console.error('Error resetting cap gates:', error); + return false; + } + + return true; +} diff --git a/lib/services/daily-mission-agent.ts b/lib/services/daily-mission-agent.ts new file mode 100644 index 0000000..27fa0b1 --- /dev/null +++ b/lib/services/daily-mission-agent.ts @@ -0,0 +1,642 @@ +/** + * 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 { createClient } 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 = createClient(); + + 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 = createClient(); + + 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 + */ +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 = createClient(); + + 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 + */ +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 = createClient(); + + 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 = createClient(); + 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 = createClient(); + + 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 = createClient(); + + // 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 = createClient(); + + 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; +} diff --git a/lib/services/proposal.service.ts b/lib/services/proposal.service.ts new file mode 100644 index 0000000..aa25237 --- /dev/null +++ b/lib/services/proposal.service.ts @@ -0,0 +1,737 @@ +/** + * Voxyz Proposal Service + * Core service for mission proposal creation, validation, and management + * Phase 10: Autonomous Architecture + */ + +import { + MissionProposal, + MissionProposalInsert, + CapGateCheckResult, + CapGateViolation, + AIAnalysisSummary, + ProposalStatus, + ProposalServiceConfig +} from '@/lib/supabase/voxyz.types'; +import { createClient } from '@/lib/supabase/client'; +import { Task } from '@/lib/data/tasks'; +import { Project } from '@/lib/data/projects'; + +const DEFAULT_CONFIG: ProposalServiceConfig = { + maxProposalsPerDay: 3, + minProposalsPerDay: 1, + defaultProposalExpiryHours: 24, + priorityScoreThreshold: 50, + autoApproveThreshold: 85, +}; + +/** + * Priority scoring factors and their weights + */ +const PRIORITY_FACTORS = { + deadlineUrgency: 0.30, + taskPriority: 0.25, + projectImportance: 0.20, + blockedStatus: 0.15, + staleTask: 0.10, +}; + +/** + * Calculate priority score for a proposal + */ +export function calculatePriorityScore( + task: Task, + project: Project | undefined, + context: ProposalContext +): number { + let score = 50; // Base score + const factors: { factor: string; weight: number; contribution: number }[] = []; + + // 1. Deadline urgency (30%) + const deadlineScore = calculateDeadlineScore(task, context); + const deadlineContribution = deadlineScore * PRIORITY_FACTORS.deadlineUrgency; + score += deadlineContribution; + factors.push({ factor: 'deadline_urgency', weight: PRIORITY_FACTORS.deadlineUrgency, contribution: deadlineContribution }); + + // 2. Task priority (25%) + const priorityScore = calculateTaskPriorityScore(task); + const priorityContribution = priorityScore * PRIORITY_FACTORS.taskPriority; + score += priorityContribution; + factors.push({ factor: 'task_priority', weight: PRIORITY_FACTORS.taskPriority, contribution: priorityContribution }); + + // 3. Project importance (20%) + const projectScore = calculateProjectImportanceScore(project); + const projectContribution = projectScore * PRIORITY_FACTORS.projectImportance; + score += projectContribution; + factors.push({ factor: 'project_importance', weight: PRIORITY_FACTORS.projectImportance, contribution: projectContribution }); + + // 4. Blocked status (15%) + const blockedScore = calculateBlockedScore(task, context); + const blockedContribution = blockedScore * PRIORITY_FACTORS.blockedStatus; + score += blockedContribution; + factors.push({ factor: 'blocked_status', weight: PRIORITY_FACTORS.blockedStatus, contribution: blockedContribution }); + + // 5. Stale task (10%) + const staleScore = calculateStaleScore(task, context); + const staleContribution = staleScore * PRIORITY_FACTORS.staleTask; + score += staleContribution; + factors.push({ factor: 'stale_task', weight: PRIORITY_FACTORS.staleTask, contribution: staleContribution }); + + return Math.min(100, Math.max(0, Math.round(score))); +} + +/** + * Calculate deadline urgency score (0-100) + */ +function calculateDeadlineScore(task: Task, context: ProposalContext): number { + if (!task.dueDate) return 0; + + const dueDate = new Date(task.dueDate); + const now = context.currentTime; + const hoursUntilDue = (dueDate.getTime() - now.getTime()) / (1000 * 60 * 60); + + if (hoursUntilDue < 0) return 100; // Overdue + if (hoursUntilDue < 24) return 90; // Due within 24 hours + if (hoursUntilDue < 72) return 70; // Due within 3 days + if (hoursUntilDue < 168) return 50; // Due within a week + if (hoursUntilDue < 336) return 30; // Due within 2 weeks + return 10; // Due later +} + +/** + * Calculate task priority score (0-100) + */ +function calculateTaskPriorityScore(task: Task): number { + const priorityMap: Record = { + 'urgent': 100, + 'high': 75, + 'medium': 50, + 'low': 25, + }; + return priorityMap[task.priority] || 50; +} + +/** + * Calculate project importance score (0-100) + */ +function calculateProjectImportanceScore(project: Project | undefined): number { + if (!project) return 50; + + // Check for high-priority keywords in project + const highPriorityKeywords = ['ios', 'revenue', 'critical', 'milestone']; + const projectNameLower = project.name.toLowerCase(); + + for (const keyword of highPriorityKeywords) { + if (projectNameLower.includes(keyword)) { + return 80; + } + } + + return 50; +} + +/** + * Calculate blocked status score (0-100) + */ +function calculateBlockedScore(task: Task, context: ProposalContext): number { + if (task.status === 'blocked') { + // Check how long it's been blocked + const blockedHours = task.comments?.reduce((hours, comment) => { + if (comment.text?.toLowerCase().includes('blocked')) { + const commentTime = new Date(comment.createdAt); + const blockedHours = (context.currentTime.getTime() - commentTime.getTime()) / (1000 * 60 * 60); + return Math.max(hours, blockedHours); + } + return hours; + }, 0) || 0; + + if (blockedHours > 48) return 100; + if (blockedHours > 24) return 80; + return 60; + } + return 0; +} + +/** + * Calculate stale task score (0-100) + */ +function calculateStaleScore(task: Task, context: ProposalContext): number { + const lastUpdate = new Date(task.updatedAt); + const daysSinceUpdate = (context.currentTime.getTime() - lastUpdate.getTime()) / (1000 * 60 * 60 * 24); + + if (task.status === 'in-progress' || task.status === 'todo') { + if (daysSinceUpdate > 14) return 100; + if (daysSinceUpdate > 7) return 75; + if (daysSinceUpdate > 3) return 50; + } + + return 0; +} + +/** + * Generate AI analysis summary for a proposal + */ +export function generateAIAnalysis( + task: Task, + project: Project | undefined, + priorityScore: number, + factors: { factor: string; weight: number; contribution: number }[] +): AIAnalysisSummary { + const urgencyScore = calculateDeadlineScore(task, { currentTime: new Date() }); + const impactScore = Math.round((priorityScore + urgencyScore) / 2); + + // Determine effort estimate based on task complexity + const effortEstimate = estimateEffort(task); + + // Generate reasoning + const reasoning = generateReasoning(task, project, factors); + + // Identify risks + const risks = identifyRisks(task, project); + + // Identify opportunities + const opportunities = identifyOpportunities(task, project); + + return { + priority_factors: factors, + urgency_score: urgencyScore, + impact_score: impactScore, + effort_estimate: effortEstimate, + confidence: calculateConfidence(task, factors), + reasoning, + risks, + opportunities, + }; +} + +/** + * Estimate effort required for a task + */ +function estimateEffort(task: Task): 'low' | 'medium' | 'high' { + const description = (task.description || '').toLowerCase(); + const title = task.title.toLowerCase(); + + // High effort indicators + const highEffortKeywords = ['refactor', 'architecture', 'redesign', 'migration', 'integrate', 'implement']; + for (const keyword of highEffortKeywords) { + if (description.includes(keyword) || title.includes(keyword)) { + return 'high'; + } + } + + // Medium effort indicators + const mediumEffortKeywords = ['update', 'modify', 'enhance', 'improve', 'fix', 'add']; + for (const keyword of mediumEffortKeywords) { + if (description.includes(keyword) || title.includes(keyword)) { + return 'medium'; + } + } + + return 'low'; +} + +/** + * Generate reasoning for the proposal + */ +function generateReasoning( + task: Task, + project: Project | undefined, + factors: { factor: string; weight: number; contribution: number }[] +): string { + const parts: string[] = []; + + // Sort factors by contribution + const sortedFactors = [...factors].sort((a, b) => b.contribution - a.contribution); + + // Add top factor + const topFactor = sortedFactors[0]; + if (topFactor) { + parts.push(`Primary driver: ${topFactor.factor.replace('_', ' ')} (+${Math.round(topFactor.contribution)} points)`); + } + + // Add deadline context + if (task.dueDate) { + const dueDate = new Date(task.dueDate); + const daysUntil = Math.ceil((dueDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)); + if (daysUntil <= 3) { + parts.push(`Urgent: Due in ${daysUntil} day${daysUntil !== 1 ? 's' : ''}`); + } + } + + // Add project context + if (project) { + parts.push(`Part of ${project.name}`); + } + + return parts.join('. '); +} + +/** + * Identify risks for the task + */ +function identifyRisks(task: Task, project: Project | undefined): string[] { + const risks: string[] = []; + + if (task.status === 'blocked') { + risks.push('Task is currently blocked'); + } + + if (task.dueDate) { + const dueDate = new Date(task.dueDate); + const daysUntil = Math.ceil((dueDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)); + if (daysUntil < 0) { + risks.push('Task is overdue'); + } else if (daysUntil <= 2) { + risks.push('Very tight deadline'); + } + } + + if (task.priority === 'urgent') { + risks.push('High priority task may have dependencies'); + } + + return risks; +} + +/** + * Identify opportunities for the task + */ +function identifyOpportunities(task: Task, project: Project | undefined): string[] { + const opportunities: string[] = []; + + if (project?.name.toLowerCase().includes('ios')) { + opportunities.push('Contributes to iOS portfolio growth'); + } + + if (task.tags.some(tag => tag.toLowerCase().includes('revenue'))) { + opportunities.push('May impact revenue directly'); + } + + if (task.tags.some(tag => tag.toLowerCase().includes('milestone'))) { + opportunities.push('Advances key milestone'); + } + + return opportunities; +} + +/** + * Calculate confidence in the proposal + */ +function calculateConfidence(task: Task, factors: { factor: string; weight: number; contribution: number }[]): number { + let confidence = 70; // Base confidence + + // Boost confidence if we have clear signals + const hasDeadline = !!task.dueDate; + const hasClearPriority = task.priority === 'urgent' || task.priority === 'high'; + const hasDescription = (task.description || '').length > 50; + + if (hasDeadline) confidence += 10; + if (hasClearPriority) confidence += 10; + if (hasDescription) confidence += 5; + + // Reduce confidence if factors are contradictory + const factorValues = factors.map(f => f.contribution); + const variance = calculateVariance(factorValues); + if (variance > 400) confidence -= 10; // High variance = conflicting signals + + return Math.min(95, Math.max(50, confidence)); +} + +/** + * Calculate variance of an array + */ +function calculateVariance(values: number[]): number { + if (values.length === 0) return 0; + const mean = values.reduce((a, b) => a + b, 0) / values.length; + const squaredDiffs = values.map(v => Math.pow(v - mean, 2)); + return squaredDiffs.reduce((a, b) => a + b, 0) / values.length; +} + +/** + * Create a mission proposal from a task + */ +export function createProposalFromTask( + task: Task, + project: Project | undefined, + context: ProposalContext, + config: ProposalServiceConfig = DEFAULT_CONFIG +): MissionProposalInsert { + const priorityScore = calculatePriorityScore(task, project, context); + const factors = getPriorityFactors(task, project, context); + const aiAnalysis = generateAIAnalysis(task, project, priorityScore, factors); + + // Estimate duration based on effort + const durationMap: Record = { + 'low': 60, + 'medium': 180, + 'high': 360, + }; + const estimatedDuration = durationMap[aiAnalysis.effort_estimate || 'medium'] || 120; + + // Calculate suggested time slot + const suggestedStartTime = calculateSuggestedStartTime(context, estimatedDuration); + const suggestedEndTime = suggestedStartTime + ? new Date(suggestedStartTime.getTime() + estimatedDuration * 60000) + : null; + + // Generate rationale + const rationale = aiAnalysis.reasoning || `High priority task: ${task.title}`; + + return { + proposal_date: context.currentTime.toISOString().split('T')[0], + status: 'pending', + priority_score: priorityScore, + passes_cap_gates: false, // Will be validated later + cap_gate_violations: [], + source_task_ids: [task.id], + source_project_ids: project ? [project.id] : [], + title: task.title, + description: task.description, + rationale, + ai_analysis: aiAnalysis, + suggested_start_time: suggestedStartTime?.toISOString() || null, + suggested_end_time: suggestedEndTime?.toISOString() || null, + estimated_duration_minutes: estimatedDuration, + expires_at: new Date(context.currentTime.getTime() + config.defaultProposalExpiryHours * 60 * 60 * 1000).toISOString(), + }; +} + +/** + * Get priority factors for a task + */ +function getPriorityFactors( + task: Task, + project: Project | undefined, + context: ProposalContext +): { factor: string; weight: number; contribution: number }[] { + const factors: { factor: string; weight: number; contribution: number }[] = []; + + const deadlineScore = calculateDeadlineScore(task, context); + factors.push({ factor: 'deadline_urgency', weight: PRIORITY_FACTORS.deadlineUrgency, contribution: deadlineScore * PRIORITY_FACTORS.deadlineUrgency }); + + const priorityScore = calculateTaskPriorityScore(task); + factors.push({ factor: 'task_priority', weight: PRIORITY_FACTORS.taskPriority, contribution: priorityScore * PRIORITY_FACTORS.taskPriority }); + + const projectScore = calculateProjectImportanceScore(project); + factors.push({ factor: 'project_importance', weight: PRIORITY_FACTORS.projectImportance, contribution: projectScore * PRIORITY_FACTORS.projectImportance }); + + const blockedScore = calculateBlockedScore(task, context); + factors.push({ factor: 'blocked_status', weight: PRIORITY_FACTORS.blockedStatus, contribution: blockedScore * PRIORITY_FACTORS.blockedStatus }); + + const staleScore = calculateStaleScore(task, context); + factors.push({ factor: 'stale_task', weight: PRIORITY_FACTORS.staleTask, contribution: staleScore * PRIORITY_FACTORS.staleTask }); + + return factors; +} + +/** + * Calculate suggested start time for a proposal + */ +function calculateSuggestedStartTime(context: ProposalContext, durationMinutes: number): Date | null { + // Start with tomorrow at 9 AM + const tomorrow = new Date(context.currentTime); + tomorrow.setDate(tomorrow.getDate() + 1); + tomorrow.setHours(9, 0, 0, 0); + + // TODO: Consider calendar availability from context + + return tomorrow; +} + +/** + * Proposal context for calculations + */ +export interface ProposalContext { + currentTime: Date; + activeTasks?: Task[]; + recentProposals?: MissionProposal[]; + calendarAvailability?: unknown[]; +} + +/** + * Validate a proposal against cap gates + */ +export async function validateProposalAgainstCapGates( + proposal: MissionProposalInsert, + existingProposals: MissionProposal[], + config: ProposalServiceConfig = DEFAULT_CONFIG +): Promise { + const violations: CapGateViolation[] = []; + const checks: CapGateCheckResult['checks'] = []; + + // Check 1: Daily mission limit + const todayProposals = existingProposals.filter(p => + p.proposal_date === proposal.proposal_date && + p.status !== 'rejected' && + p.status !== 'expired' + ); + + const dailyLimitGate = { + gate: { + id: 'daily_limit', + name: 'Daily Mission Limit', + description: 'Maximum number of missions per day', + gate_type: 'daily_mission_limit' as const, + max_value: config.maxProposalsPerDay, + min_value: config.minProposalsPerDay, + unit: 'count', + current_value: todayProposals.length, + last_evaluated_at: new Date().toISOString(), + policy_id: null, + is_active: true, + is_blocking: false, + evaluation_window_hours: 24, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + created_by: null, + updated_by: null, + }, + passed: todayProposals.length < config.maxProposalsPerDay, + currentValue: todayProposals.length, + }; + + if (!dailyLimitGate.passed) { + dailyLimitGate.gate.is_blocking = true; + violations.push({ + gate_name: dailyLimitGate.gate.name, + gate_type: dailyLimitGate.gate.gate_type, + current_value: dailyLimitGate.currentValue, + max_value: config.maxProposalsPerDay, + violation_message: `Daily mission limit of ${config.maxProposalsPerDay} reached`, + }); + } + + checks.push(dailyLimitGate); + + // Check 2: Workload capacity + const totalWorkload = todayProposals.reduce((sum, p) => + sum + (p.estimated_duration_minutes || 120), 0 + ); + const proposedWorkload = totalWorkload + (proposal.estimated_duration_minutes || 120); + const maxWorkloadMinutes = 480; // 8 hours + + const workloadGate = { + gate: { + id: 'workload_capacity', + name: 'Workload Capacity', + description: 'Daily workload capacity in minutes', + gate_type: 'workload_capacity' as const, + max_value: maxWorkloadMinutes, + min_value: 0, + unit: 'minutes', + current_value: proposedWorkload, + last_evaluated_at: new Date().toISOString(), + policy_id: null, + is_active: true, + is_blocking: false, + evaluation_window_hours: 24, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + created_by: null, + updated_by: null, + }, + passed: proposedWorkload <= maxWorkloadMinutes, + currentValue: proposedWorkload, + }; + + if (!workloadGate.passed) { + workloadGate.gate.is_blocking = true; + violations.push({ + gate_name: workloadGate.gate.name, + gate_type: workloadGate.gate.gate_type, + current_value: proposedWorkload, + max_value: maxWorkloadMinutes, + violation_message: `Workload would exceed ${maxWorkloadMinutes} minutes (${Math.round(proposedWorkload)} proposed)`, + }); + } + + checks.push(workloadGate); + + // Check 3: Priority threshold + if (proposal.priority_score < config.priorityScoreThreshold) { + violations.push({ + gate_name: 'Priority Threshold', + gate_type: 'prioritization', + current_value: proposal.priority_score, + max_value: config.priorityScoreThreshold, + violation_message: `Priority score ${proposal.priority_score} below threshold ${config.priorityScoreThreshold}`, + }); + } + + return { + allPassed: violations.length === 0, + checks, + violations, + summary: violations.length === 0 + ? 'All cap gates passed' + : `${violations.length} cap gate violation${violations.length !== 1 ? 's' : ''}`, + }; +} + +/** + * Save a proposal to the database + */ +export async function saveProposal( + proposal: MissionProposalInsert, + capGateResult: CapGateCheckResult +): Promise { + const supabase = createClient(); + + const proposalWithCapGates: MissionProposalInsert = { + ...proposal, + passes_cap_gates: capGateResult.allPassed, + cap_gate_violations: capGateResult.violations, + }; + + const { data, error } = await supabase + .from('ops_mission_proposals') + .insert(proposalWithCapGates) + .select() + .single(); + + if (error) { + console.error('Error saving proposal:', error); + return null; + } + + return data as MissionProposal; +} + +/** + * Get proposals for a specific date + */ +export async function getProposalsForDate(date: Date): Promise { + const supabase = createClient(); + const dateString = date.toISOString().split('T')[0]; + + const { data, error } = await supabase + .from('ops_mission_proposals') + .select('*') + .eq('proposal_date', dateString) + .order('priority_score', { ascending: false }); + + if (error) { + console.error('Error fetching proposals:', error); + return []; + } + + return (data || []) as MissionProposal[]; +} + +/** + * Approve a proposal + */ +export async function approveProposal( + proposalId: string, + userId: string +): Promise { + const supabase = createClient(); + + const { error } = await supabase + .from('ops_mission_proposals') + .update({ + status: 'approved', + approved_at: new Date().toISOString(), + approved_by: userId, + }) + .eq('id', proposalId); + + if (error) { + console.error('Error approving proposal:', error); + return false; + } + + return true; +} + +/** + * Reject a proposal + */ +export async function rejectProposal( + proposalId: string, + reason: string +): Promise { + const supabase = createClient(); + + const { error } = await supabase + .from('ops_mission_proposals') + .update({ + status: 'rejected', + rejected_at: new Date().toISOString(), + rejection_reason: reason, + }) + .eq('id', proposalId); + + if (error) { + console.error('Error rejecting proposal:', error); + return false; + } + + return true; +} + +/** + * Mark proposal as completed + */ +export async function completeProposal( + proposalId: string, + notes?: string +): Promise { + const supabase = createClient(); + + const { error } = await supabase + .from('ops_mission_proposals') + .update({ + status: 'completed', + executed_at: new Date().toISOString(), + execution_notes: notes || null, + }) + .eq('id', proposalId); + + if (error) { + console.error('Error completing proposal:', error); + return false; + } + + return true; +} + +/** + * Get top proposals for today + */ +export async function getTopProposalsForToday(limit: number = 3): Promise { + const today = new Date(); + const proposals = await getProposalsForDate(today); + + // Filter to pending and approved proposals + const activeProposals = proposals.filter(p => + p.status === 'pending' || p.status === 'approved' + ); + + // Sort by priority score + activeProposals.sort((a, b) => b.priority_score - a.priority_score); + + return activeProposals.slice(0, limit); +} + +/** + * Expire old proposals + */ +export async function expireOldProposals(): Promise { + const supabase = createClient(); + const now = new Date().toISOString(); + + const { data, error } = await supabase + .from('ops_mission_proposals') + .update({ status: 'expired' }) + .lt('expires_at', now) + .eq('status', 'pending') + .select('id'); + + if (error) { + console.error('Error expiring proposals:', error); + return 0; + } + + return data?.length || 0; +} diff --git a/lib/services/trigger-rules.service.ts b/lib/services/trigger-rules.service.ts new file mode 100644 index 0000000..b60d8d1 --- /dev/null +++ b/lib/services/trigger-rules.service.ts @@ -0,0 +1,587 @@ +/** + * 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 { createClient } 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 = createClient(); + + 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.created_at || 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 = createClient(); + 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 = createClient(); + + 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 = createClient(); + + 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 = createClient(); + + 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 = createClient(); + + 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 || []; +} diff --git a/lib/supabase/migrations/001_voxyz_phase10_schema.sql b/lib/supabase/migrations/001_voxyz_phase10_schema.sql new file mode 100644 index 0000000..8aae31f --- /dev/null +++ b/lib/supabase/migrations/001_voxyz_phase10_schema.sql @@ -0,0 +1,504 @@ +-- Voxyz Autonomous Architecture - Phase 10 Database Schema +-- Creates tables for Proposal Service, Policy System, Trigger Rules, and Daily Missions + +-- Enable UUID extension +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- ============================================ +-- 1. MISSION PROPOSALS TABLE +-- Stores AI-generated daily mission proposals +-- ============================================ +CREATE TABLE IF NOT EXISTS ops_mission_proposals ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + + -- Proposal metadata + proposal_date DATE NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'pending' + CHECK (status IN ('pending', 'approved', 'rejected', 'scheduled', 'completed', 'expired')), + + -- Priority scoring (0-100) + priority_score INTEGER NOT NULL DEFAULT 50 + CHECK (priority_score >= 0 AND priority_score <= 100), + + -- Cap Gates compliance + passes_cap_gates BOOLEAN NOT NULL DEFAULT false, + cap_gate_violations JSONB DEFAULT '[]'::jsonb, + + -- Source task references (links to gantt tasks) + source_task_ids TEXT[] DEFAULT '{}', + source_project_ids TEXT[] DEFAULT '{}', + + -- Proposal content + title TEXT NOT NULL, + description TEXT, + rationale TEXT, + + -- AI analysis summary + ai_analysis JSONB DEFAULT '{}'::jsonb, + + -- Scheduling + suggested_start_time TIMESTAMP WITH TIME ZONE, + suggested_end_time TIMESTAMP WITH TIME ZONE, + estimated_duration_minutes INTEGER, + + -- User decisions + approved_at TIMESTAMP WITH TIME ZONE, + approved_by UUID REFERENCES users(id), + rejected_at TIMESTAMP WITH TIME ZONE, + rejection_reason TEXT, + + -- Calendar integration + calendar_event_id TEXT, + calendar_synced_at TIMESTAMP WITH TIME ZONE, + + -- Execution tracking + executed_at TIMESTAMP WITH TIME ZONE, + execution_notes TEXT, + + -- Timestamps for lifecycle + expires_at TIMESTAMP WITH TIME ZONE NOT NULL, + + -- Indexes + CONSTRAINT valid_dates CHECK (suggested_end_time > suggested_start_time OR suggested_start_time IS NULL) +); + +-- Create indexes for mission_proposals +CREATE INDEX IF NOT EXISTS idx_proposals_status ON ops_mission_proposals(status); +CREATE INDEX IF NOT EXISTS idx_proposals_date ON ops_mission_proposals(proposal_date); +CREATE INDEX IF NOT EXISTS idx_proposals_priority ON ops_mission_proposals(priority_score DESC); +CREATE INDEX IF NOT EXISTS idx_proposals_created_at ON ops_mission_proposals(created_at DESC); + +-- ============================================ +-- 2. POLICY TABLE +-- Stores operational policies and constraints +-- ============================================ +CREATE TABLE IF NOT EXISTS ops_policy ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + + -- Policy metadata + name VARCHAR(100) NOT NULL UNIQUE, + description TEXT, + category VARCHAR(50) NOT NULL + CHECK (category IN ('cap_gates', 'scheduling', 'prioritization', 'execution', 'general')), + + -- Policy configuration + config JSONB NOT NULL DEFAULT '{}'::jsonb, + + -- Policy rules (stored as JSON for flexibility) + rules JSONB NOT NULL DEFAULT '{}'::jsonb, + + -- Status + is_active BOOLEAN NOT NULL DEFAULT true, + effective_from TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + effective_until TIMESTAMP WITH TIME ZONE, + + -- Versioning + version INTEGER NOT NULL DEFAULT 1, + previous_version_id UUID REFERENCES ops_policy(id), + + -- Audit + created_by UUID REFERENCES users(id), + updated_by UUID REFERENCES users(id) +); + +-- Create indexes for policy +CREATE INDEX IF NOT EXISTS idx_policy_category ON ops_policy(category); +CREATE INDEX IF NOT EXISTS idx_policy_active ON ops_policy(is_active) WHERE is_active = true; + +-- ============================================ +-- 3. TRIGGER RULES TABLE +-- Stores automated trigger rules for the system +-- ============================================ +CREATE TABLE IF NOT EXISTS ops_trigger_rules ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + + -- Rule metadata + name VARCHAR(100) NOT NULL, + description TEXT, + + -- Trigger type + trigger_type VARCHAR(50) NOT NULL + CHECK (trigger_type IN ('deadline_approaching', 'task_blocked', 'high_priority_created', + 'stale_task', 'sprint_ending', 'milestone_reached', + 'daily_mission_time', 'manual', 'calendar_conflict')), + + -- Trigger conditions (JSON for complex conditions) + conditions JSONB NOT NULL DEFAULT '{}'::jsonb, + + -- Action to take when triggered + action_type VARCHAR(50) NOT NULL + CHECK (action_type IN ('create_proposal', 'send_notification', 'escalate_priority', + 'auto_schedule', 'request_review', 'update_status')), + + action_config JSONB NOT NULL DEFAULT '{}'::jsonb, + + -- Priority for rule evaluation order + priority INTEGER NOT NULL DEFAULT 100, + + -- Status + is_active BOOLEAN NOT NULL DEFAULT true, + + -- Rate limiting + cooldown_minutes INTEGER DEFAULT 60, + max_triggers_per_day INTEGER DEFAULT 10, + + -- Tracking + last_triggered_at TIMESTAMP WITH TIME ZONE, + trigger_count INTEGER DEFAULT 0, + + -- Audit + created_by UUID REFERENCES users(id), + updated_by UUID REFERENCES users(id) +); + +-- Create indexes for trigger_rules +CREATE INDEX IF NOT EXISTS idx_trigger_rules_type ON ops_trigger_rules(trigger_type); +CREATE INDEX IF NOT EXISTS idx_trigger_rules_active ON ops_trigger_rules(is_active) WHERE is_active = true; +CREATE INDEX IF NOT EXISTS idx_trigger_rules_priority ON ops_trigger_rules(priority); + +-- ============================================ +-- 4. CAP GATES TABLE +-- Stores queue management cap gates +-- ============================================ +CREATE TABLE IF NOT EXISTS ops_cap_gates ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + + -- Gate metadata + name VARCHAR(100) NOT NULL UNIQUE, + description TEXT, + + -- Gate type + gate_type VARCHAR(50) NOT NULL + CHECK (gate_type IN ('daily_mission_limit', 'concurrent_high_priority', + 'workload_capacity', 'focus_time_minimum', + 'calendar_buffer', 'reaction_ratio')), + + -- Configuration + max_value INTEGER NOT NULL, + min_value INTEGER DEFAULT 0, + unit VARCHAR(20) DEFAULT 'count', + + -- Current state + current_value INTEGER DEFAULT 0, + last_evaluated_at TIMESTAMP WITH TIME ZONE, + + -- Policy reference + policy_id UUID REFERENCES ops_policy(id), + + -- Status + is_active BOOLEAN NOT NULL DEFAULT true, + is_blocking BOOLEAN NOT NULL DEFAULT false, + + -- Evaluation window + evaluation_window_hours INTEGER DEFAULT 24, + + -- Audit + created_by UUID REFERENCES users(id), + updated_by UUID REFERENCES users(id) +); + +-- Create indexes for cap_gates +CREATE INDEX IF NOT EXISTS idx_cap_gates_type ON ops_cap_gates(gate_type); +CREATE INDEX IF NOT EXISTS idx_cap_gates_active ON ops_cap_gates(is_active) WHERE is_active = true; + +-- ============================================ +-- 5. DAILY MISSIONS TABLE +-- Stores generated and scheduled daily missions +-- ============================================ +CREATE TABLE IF NOT EXISTS ops_daily_missions ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + + -- Mission date + mission_date DATE NOT NULL, + + -- Generation metadata + generated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + generated_by VARCHAR(50) DEFAULT 'ai_agent' + CHECK (generated_by IN ('ai_agent', 'manual', 'trigger')), + + -- Mission status + status VARCHAR(20) NOT NULL DEFAULT 'draft' + CHECK (status IN ('draft', 'review', 'approved', 'scheduled', 'in_progress', 'completed', 'canceled')), + + -- Proposal references (up to 3 missions per day) + primary_mission_id UUID REFERENCES ops_mission_proposals(id), + secondary_mission_id UUID REFERENCES ops_mission_proposals(id), + tertiary_mission_id UUID REFERENCES ops_mission_proposals(id), + + -- Total estimated time + total_estimated_minutes INTEGER, + + -- Focus time allocation + focus_time_start TIMESTAMP WITH TIME ZONE, + focus_time_end TIMESTAMP WITH TIME ZONE, + focus_time_minutes INTEGER, + + -- Calendar blocks + calendar_blocks JSONB DEFAULT '[]'::jsonb, + + -- Reaction matrix tracking + planned_reactions INTEGER DEFAULT 0, + actual_reactions INTEGER DEFAULT 0, + reaction_ratio DECIMAL(3,2) DEFAULT 0.00, + + -- Completion tracking + completed_at TIMESTAMP WITH TIME ZONE, + completion_notes TEXT, + completion_rating INTEGER CHECK (completion_rating >= 1 AND completion_rating <= 5), + + -- Feedback for AI learning + user_feedback JSONB DEFAULT '{}'::jsonb, + + -- Unique constraint for one mission set per day + CONSTRAINT unique_daily_mission UNIQUE (mission_date) +); + +-- Create indexes for daily_missions +CREATE INDEX IF NOT EXISTS idx_daily_missions_date ON ops_daily_missions(mission_date); +CREATE INDEX IF NOT EXISTS idx_daily_missions_status ON ops_daily_missions(status); + +-- ============================================ +-- 6. CALENDAR BLOCKS TABLE +-- Stores scheduled focus time blocks +-- ============================================ +CREATE TABLE IF NOT EXISTS ops_calendar_blocks ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + + -- Block metadata + block_date DATE NOT NULL, + + -- Time range + start_time TIMESTAMP WITH TIME ZONE NOT NULL, + end_time TIMESTAMP WITH TIME ZONE NOT NULL, + + -- Block type + block_type VARCHAR(50) NOT NULL + CHECK (block_type IN ('deep_work', 'mission_execution', 'planning', + 'reaction_time', 'buffer', 'meeting')), + + -- Linked mission + mission_id UUID REFERENCES ops_daily_missions(id), + proposal_id UUID REFERENCES ops_mission_proposals(id), + + -- External calendar integration + calendar_event_id TEXT, + calendar_provider VARCHAR(20) DEFAULT 'google' + CHECK (calendar_provider IN ('google', 'outlook', 'apple', 'other')), + calendar_synced BOOLEAN DEFAULT false, + calendar_synced_at TIMESTAMP WITH TIME ZONE, + + -- Status + status VARCHAR(20) DEFAULT 'scheduled' + CHECK (status IN ('scheduled', 'active', 'completed', 'canceled', 'rescheduled')), + + -- Notes + notes TEXT, + + CONSTRAINT valid_block_times CHECK (end_time > start_time) +); + +-- Create indexes for calendar_blocks +CREATE INDEX IF NOT EXISTS idx_calendar_blocks_date ON ops_calendar_blocks(block_date); +CREATE INDEX IF NOT EXISTS idx_calendar_blocks_mission ON ops_calendar_blocks(mission_id); +CREATE INDEX IF NOT EXISTS idx_calendar_blocks_status ON ops_calendar_blocks(status); + +-- ============================================ +-- 7. TRIGGER LOG TABLE +-- Stores trigger rule execution history +-- ============================================ +CREATE TABLE IF NOT EXISTS ops_trigger_log ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + + -- Trigger reference + rule_id UUID REFERENCES ops_trigger_rules(id), + rule_name VARCHAR(100), + + -- Execution details + triggered_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + trigger_type VARCHAR(50) NOT NULL, + + -- Context + context JSONB DEFAULT '{}'::jsonb, + task_ids TEXT[] DEFAULT '{}', + + -- Result + action_taken VARCHAR(50), + action_result TEXT, + success BOOLEAN DEFAULT true, + error_message TEXT, + + -- Proposal created (if applicable) + proposal_id UUID REFERENCES ops_mission_proposals(id) +); + +-- Create indexes for trigger_log +CREATE INDEX IF NOT EXISTS idx_trigger_log_rule ON ops_trigger_log(rule_id); +CREATE INDEX IF NOT EXISTS idx_trigger_log_created ON ops_trigger_log(created_at DESC); +CREATE INDEX IF NOT EXISTS idx_trigger_log_type ON ops_trigger_log(trigger_type); + +-- ============================================ +-- 8. AGENT EXECUTION LOG TABLE +-- Stores AI agent execution history +-- ============================================ +CREATE TABLE IF NOT EXISTS ops_agent_execution_log ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + + -- Execution metadata + agent_name VARCHAR(50) NOT NULL + CHECK (agent_name IN ('proposal_generator', 'cap_gate_validator', 'calendar_scheduler', + 'trigger_monitor', 'mission_optimizer', 'stale_task_detector')), + + execution_type VARCHAR(50) NOT NULL + CHECK (execution_type IN ('scheduled', 'triggered', 'manual', 'reaction')), + + -- Execution details + started_at TIMESTAMP WITH TIME ZONE NOT NULL, + completed_at TIMESTAMP WITH TIME ZONE, + duration_ms INTEGER, + + -- Input/Output + input_summary JSONB DEFAULT '{}'::jsonb, + output_summary JSONB DEFAULT '{}'::jsonb, + + -- Results + proposals_created INTEGER DEFAULT 0, + proposals_approved INTEGER DEFAULT 0, + tasks_analyzed INTEGER DEFAULT 0, + + -- Status + status VARCHAR(20) DEFAULT 'running' + CHECK (status IN ('running', 'success', 'partial', 'failed')), + error_message TEXT, + + -- Performance metrics + ai_tokens_used INTEGER, + ai_cost_usd DECIMAL(10,4) +); + +-- Create indexes for agent_execution_log +CREATE INDEX IF NOT EXISTS idx_agent_log_agent ON ops_agent_execution_log(agent_name); +CREATE INDEX IF NOT EXISTS idx_agent_log_created ON ops_agent_execution_log(created_at DESC); +CREATE INDEX IF NOT EXISTS idx_agent_log_status ON ops_agent_execution_log(status); + +-- ============================================ +-- DEFAULT POLICY INSERTS +-- ============================================ + +-- Insert default Cap Gates policies +INSERT INTO ops_policy (name, description, category, config, rules) VALUES +('daily_mission_limit', 'Maximum number of missions per day', 'cap_gates', + '{"max_missions_per_day": 3, "min_missions_per_day": 1}'::jsonb, + '{"enforce": true, "allow_override": false}'::jsonb), + +('concurrent_high_priority', 'Limit on concurrent high-priority work', 'cap_gates', + '{"max_concurrent_high_priority": 2, "high_priority_threshold": 75}'::jsonb, + '{"enforce": true, "escalation_required": true}'::jsonb), + +('workload_capacity', 'Daily workload capacity in minutes', 'cap_gates', + '{"max_daily_minutes": 480, "min_focus_block_minutes": 90}'::jsonb, + '{"enforce": true, "buffer_percentage": 20}'::jsonb), + +('focus_time_minimum', 'Minimum required focus time per day', 'cap_gates', + '{"min_focus_minutes": 120, "preferred_focus_start": "09:00", "preferred_focus_end": "17:00"}'::jsonb, + '{"enforce": true, "auto_protect": true}'::jsonb), + +('calendar_buffer', 'Buffer time between calendar events', 'cap_gates', + '{"min_buffer_minutes": 15, "default_buffer_minutes": 30}'::jsonb, + '{"enforce": true, "auto_add_buffer": true}'::jsonb), + +('reaction_ratio', 'Percentage of time reserved for spontaneous reactions', 'cap_gates', + '{"reaction_ratio": 0.30, "min_reaction_minutes": 60}'::jsonb, + '{"enforce": false, "track_only": true}'::jsonb); + +-- Insert default Trigger Rules +INSERT INTO ops_trigger_rules (name, description, trigger_type, conditions, action_type, action_config, priority) VALUES +('deadline_approaching_24h', 'Trigger when deadline is within 24 hours', 'deadline_approaching', + '{"hours_before": 24, "priority": "high"}'::jsonb, 'create_proposal', + '{"priority_boost": 20, "rationale_template": "Deadline approaching in {{hours}} hours"}'::jsonb, 100), + +('deadline_approaching_72h', 'Trigger when deadline is within 72 hours', 'deadline_approaching', + '{"hours_before": 72, "priority": "medium"}'::jsonb, 'create_proposal', + '{"priority_boost": 10, "rationale_template": "Deadline approaching in {{hours}} hours"}'::jsonb, 90), + +('task_blocked_24h', 'Trigger when task has been blocked for 24+ hours', 'task_blocked', + '{"blocked_hours": 24}'::jsonb, 'create_proposal', + '{"action": "escalate_and_propose_alternative", "priority_boost": 30}'::jsonb, 95), + +('high_priority_created', 'Trigger when high priority task is created', 'high_priority_created', + '{"priority": "urgent"}'::jsonb, 'create_proposal', + '{"immediate_review": true, "priority_boost": 25}'::jsonb, 110), + +('stale_task_7d', 'Trigger when task is stale for 7+ days', 'stale_task', + '{"stale_days": 7, "status_in": ["todo", "in-progress"]}'::jsonb, 'create_proposal', + '{"action": "review_and_reprioritize", "priority_boost": 15}'::jsonb, 80), + +('stale_task_14d', 'Trigger when task is stale for 14+ days', 'stale_task', + '{"stale_days": 14, "status_in": ["todo", "in-progress"]}'::jsonb, 'create_proposal', + '{"action": "flag_for_review", "priority_boost": 25}'::jsonb, 85), + +('sprint_ending_3d', 'Trigger when sprint ends in 3 days', 'sprint_ending', + '{"days_before": 3}'::jsonb, 'create_proposal', + '{"focus": "sprint_completion", "priority_boost": 20}'::jsonb, 88), + +('daily_mission_generation', 'Trigger daily mission generation at 7 AM', 'daily_mission_time', + '{"hour": 7, "minute": 0}'::jsonb, 'create_proposal', + '{"generate_count": 3, "include_reactions": true}'::jsonb, 120); + +-- Insert default Cap Gates +INSERT INTO ops_cap_gates (name, description, gate_type, max_value, min_value, unit, policy_id) VALUES +('daily_mission_count', 'Maximum missions per day', 'daily_mission_limit', 3, 1, 'count', + (SELECT id FROM ops_policy WHERE name = 'daily_mission_limit')), + +('concurrent_high_priority', 'Concurrent high priority items', 'concurrent_high_priority', 2, 0, 'count', + (SELECT id FROM ops_policy WHERE name = 'concurrent_high_priority')), + +('daily_workload', 'Daily workload capacity', 'workload_capacity', 480, 240, 'minutes', + (SELECT id FROM ops_policy WHERE name = 'workload_capacity')), + +('daily_focus_time', 'Minimum daily focus time', 'focus_time_minimum', 480, 120, 'minutes', + (SELECT id FROM ops_policy WHERE name = 'focus_time_minimum')), + +('reaction_time_reserve', 'Time reserved for reactions', 'reaction_ratio', 144, 60, 'minutes', + (SELECT id FROM ops_policy WHERE name = 'reaction_ratio')); + +-- Create updated_at trigger function +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- Create updated_at triggers for all tables +CREATE TRIGGER update_ops_mission_proposals_updated_at BEFORE UPDATE ON ops_mission_proposals + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_ops_policy_updated_at BEFORE UPDATE ON ops_policy + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_ops_trigger_rules_updated_at BEFORE UPDATE ON ops_trigger_rules + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_ops_cap_gates_updated_at BEFORE UPDATE ON ops_cap_gates + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_ops_daily_missions_updated_at BEFORE UPDATE ON ops_daily_missions + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_ops_calendar_blocks_updated_at BEFORE UPDATE ON ops_calendar_blocks + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); diff --git a/lib/supabase/voxyz.types.ts b/lib/supabase/voxyz.types.ts new file mode 100644 index 0000000..74fec4b --- /dev/null +++ b/lib/supabase/voxyz.types.ts @@ -0,0 +1,440 @@ +/** + * Voxyz Autonomous Architecture - Database Types Extension + * Phase 10: Mission Control Autonomous System + */ + +import { Database as BaseDatabase } from './database.types'; + +// ============================================ +// ENUM TYPES +// ============================================ + +export type ProposalStatus = + | 'pending' + | 'approved' + | 'rejected' + | 'scheduled' + | 'completed' + | 'expired'; + +export type PolicyCategory = + | 'cap_gates' + | 'scheduling' + | 'prioritization' + | 'execution' + | 'general'; + +export type TriggerType = + | 'deadline_approaching' + | 'task_blocked' + | 'high_priority_created' + | 'stale_task' + | 'sprint_ending' + | 'milestone_reached' + | 'daily_mission_time' + | 'manual' + | 'calendar_conflict'; + +export type ActionType = + | 'create_proposal' + | 'send_notification' + | 'escalate_priority' + | 'auto_schedule' + | 'request_review' + | 'update_status'; + +export type GateType = + | 'daily_mission_limit' + | 'concurrent_high_priority' + | 'workload_capacity' + | 'focus_time_minimum' + | 'calendar_buffer' + | 'reaction_ratio'; + +export type MissionStatus = + | 'draft' + | 'review' + | 'approved' + | 'scheduled' + | 'in_progress' + | 'completed' + | 'canceled'; + +export type BlockType = + | 'deep_work' + | 'mission_execution' + | 'planning' + | 'reaction_time' + | 'buffer' + | 'meeting'; + +export type AgentName = + | 'proposal_generator' + | 'cap_gate_validator' + | 'calendar_scheduler' + | 'trigger_monitor' + | 'mission_optimizer' + | 'stale_task_detector'; + +export type CalendarProvider = 'google' | 'outlook' | 'apple' | 'other'; + +// ============================================ +// TABLE ROW TYPES +// ============================================ + +export interface MissionProposal { + id: string; + created_at: string; + updated_at: string; + proposal_date: string; + status: ProposalStatus; + priority_score: number; + passes_cap_gates: boolean; + cap_gate_violations: CapGateViolation[]; + source_task_ids: string[]; + source_project_ids: string[]; + title: string; + description: string | null; + rationale: string | null; + ai_analysis: AIAnalysisSummary; + suggested_start_time: string | null; + suggested_end_time: string | null; + estimated_duration_minutes: number | null; + approved_at: string | null; + approved_by: string | null; + rejected_at: string | null; + rejection_reason: string | null; + calendar_event_id: string | null; + calendar_synced_at: string | null; + executed_at: string | null; + execution_notes: string | null; + expires_at: string; +} + +export interface Policy { + id: string; + created_at: string; + updated_at: string; + name: string; + description: string | null; + category: PolicyCategory; + config: Record; + rules: Record; + is_active: boolean; + effective_from: string; + effective_until: string | null; + version: number; + previous_version_id: string | null; + created_by: string | null; + updated_by: string | null; +} + +export interface TriggerRule { + id: string; + created_at: string; + updated_at: string; + name: string; + description: string | null; + trigger_type: TriggerType; + conditions: TriggerConditions; + action_type: ActionType; + action_config: Record; + priority: number; + is_active: boolean; + cooldown_minutes: number; + max_triggers_per_day: number; + last_triggered_at: string | null; + trigger_count: number; + created_by: string | null; + updated_by: string | null; +} + +export interface CapGate { + id: string; + created_at: string; + updated_at: string; + name: string; + description: string | null; + gate_type: GateType; + max_value: number; + min_value: number; + unit: string; + current_value: number; + last_evaluated_at: string | null; + policy_id: string | null; + is_active: boolean; + is_blocking: boolean; + evaluation_window_hours: number; + created_by: string | null; + updated_by: string | null; +} + +export interface DailyMission { + id: string; + created_at: string; + updated_at: string; + mission_date: string; + generated_at: string; + generated_by: 'ai_agent' | 'manual' | 'trigger'; + status: MissionStatus; + primary_mission_id: string | null; + secondary_mission_id: string | null; + tertiary_mission_id: string | null; + total_estimated_minutes: number | null; + focus_time_start: string | null; + focus_time_end: string | null; + focus_time_minutes: number | null; + calendar_blocks: CalendarBlock[]; + planned_reactions: number; + actual_reactions: number; + reaction_ratio: number; + completed_at: string | null; + completion_notes: string | null; + completion_rating: number | null; + user_feedback: Record; +} + +export interface CalendarBlock { + id: string; + created_at: string; + updated_at: string; + block_date: string; + start_time: string; + end_time: string; + block_type: BlockType; + mission_id: string | null; + proposal_id: string | null; + calendar_event_id: string | null; + calendar_provider: CalendarProvider; + calendar_synced: boolean; + calendar_synced_at: string | null; + status: 'scheduled' | 'active' | 'completed' | 'canceled' | 'rescheduled'; + notes: string | null; +} + +export interface TriggerLog { + id: string; + created_at: string; + rule_id: string | null; + rule_name: string; + triggered_at: string; + trigger_type: TriggerType; + context: Record; + task_ids: string[]; + action_taken: string | null; + action_result: string | null; + success: boolean; + error_message: string | null; + proposal_id: string | null; +} + +export interface AgentExecutionLog { + id: string; + created_at: string; + agent_name: AgentName; + execution_type: 'scheduled' | 'triggered' | 'manual' | 'reaction'; + started_at: string; + completed_at: string | null; + duration_ms: number | null; + input_summary: Record; + output_summary: Record; + proposals_created: number; + proposals_approved: number; + tasks_analyzed: number; + status: 'running' | 'success' | 'partial' | 'failed'; + error_message: string | null; + ai_tokens_used: number | null; + ai_cost_usd: number | null; +} + +// ============================================ +// JSON/Complex TYPES +// ============================================ + +export interface CapGateViolation { + gate_name: string; + gate_type: GateType; + current_value: number; + max_value: number; + violation_message: string; +} + +export interface AIAnalysisSummary { + priority_factors?: PriorityFactor[]; + urgency_score?: number; + impact_score?: number; + effort_estimate?: 'low' | 'medium' | 'high'; + confidence?: number; + reasoning?: string; + similar_completed_tasks?: string[]; + risks?: string[]; + opportunities?: string[]; +} + +export interface PriorityFactor { + factor: string; + weight: number; + contribution: number; +} + +export interface TriggerConditions { + // Deadline approaching + hours_before?: number; + priority?: string; + + // Task blocked + blocked_hours?: number; + + // Stale task + stale_days?: number; + status_in?: string[]; + + // Sprint ending + days_before?: number; + + // Daily mission time + hour?: number; + minute?: number; + + // Custom conditions + [key: string]: unknown; +} + +// ============================================ +// INPUT/INSERT TYPES +// ============================================ + +export type MissionProposalInsert = Omit; +export type PolicyInsert = Omit; +export type TriggerRuleInsert = Omit; +export type CapGateInsert = Omit; +export type DailyMissionInsert = Omit; +export type CalendarBlockInsert = Omit; + +// ============================================ +// DOMAIN-SPECIFIC TYPES +// ============================================ + +export interface CapGateCheck { + gate: CapGate; + passed: boolean; + currentValue: number; + violation?: CapGateViolation; +} + +export interface CapGateCheckResult { + allPassed: boolean; + checks: CapGateCheck[]; + violations: CapGateViolation[]; + summary: string; +} + +export interface MissionProposalInput { + title: string; + description?: string; + sourceTaskIds: string[]; + sourceProjectIds: string[]; + suggestedStartTime?: Date; + suggestedEndTime?: Date; + estimatedDurationMinutes?: number; + aiAnalysis?: AIAnalysisSummary; +} + +export interface TriggerEvaluationContext { + currentTime: Date; + activeTasks: BaseDatabase['public']['Tables']['tasks']['Row'][]; + recentProposals: MissionProposal[]; + todayMissions: DailyMission[]; + userAvailability: CalendarBlock[]; +} + +export interface TriggerEvaluationResult { + triggered: boolean; + rule: TriggerRule; + context: Record; + proposalInput?: MissionProposalInput; + priorityBoost?: number; +} + +export interface DailyMissionGenerationInput { + date: Date; + targetMissionCount: number; + includeReactions: boolean; + forceRegenerate?: boolean; +} + +export interface DailyMissionGenerationResult { + dailyMission: DailyMission; + proposals: MissionProposal[]; + capGatesChecked: CapGateCheckResult; + calendarBlocks: CalendarBlock[]; + executionLogId: string; +} + +export interface ProposalServiceConfig { + maxProposalsPerDay: number; + minProposalsPerDay: number; + defaultProposalExpiryHours: number; + priorityScoreThreshold: number; + autoApproveThreshold: number; +} + +export interface CapGateConfig { + dailyMissionLimit: number; + concurrentHighPriorityLimit: number; + maxDailyWorkloadMinutes: number; + minFocusTimeMinutes: number; + minCalendarBufferMinutes: number; + reactionRatio: number; +} + +// ============================================ +// EXTENDED DATABASE INTERFACE +// ============================================ + +export interface VoxyzDatabase extends BaseDatabase { + public: { + Tables: BaseDatabase['public']['Tables'] & { + ops_mission_proposals: { + Row: MissionProposal; + Insert: MissionProposalInsert; + Update: Partial; + }; + ops_policy: { + Row: Policy; + Insert: PolicyInsert; + Update: Partial; + }; + ops_trigger_rules: { + Row: TriggerRule; + Insert: TriggerRuleInsert; + Update: Partial; + }; + ops_cap_gates: { + Row: CapGate; + Insert: CapGateInsert; + Update: Partial; + }; + ops_daily_missions: { + Row: DailyMission; + Insert: DailyMissionInsert; + Update: Partial; + }; + ops_calendar_blocks: { + Row: CalendarBlock; + Insert: CalendarBlockInsert; + Update: Partial; + }; + ops_trigger_log: { + Row: TriggerLog; + Insert: Omit; + Update: Partial>; + }; + ops_agent_execution_log: { + Row: AgentExecutionLog; + Insert: Omit; + Update: Partial>; + }; + }; + }; +}
High Priority
View urgent tasks
Urgent/high in current sprint
In Progress
Active tasks
Current sprint in-progress tasks