Signed-off-by: OpenClaw Bot <ai-agent@topdoglabs.com>

This commit is contained in:
OpenClaw Bot 2026-02-25 15:40:07 -06:00
parent 0092b318c2
commit 59f85de572
10 changed files with 3840 additions and 4 deletions

View File

@ -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)

View File

@ -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 }
);
}
}

View File

@ -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 }
);
}
}

View File

@ -572,26 +572,34 @@ export default async function ProjectsOverviewPage() {
</Button>
</Link>
<Link href={getGanttTasksUrl({ priority: "urgent,high" })} target="_blank" className="block">
<Link
href={getGanttTasksUrl({ priority: "urgent,high", scope: "active-sprint" })}
target="_blank"
className="block"
>
<Button variant="outline" className="w-full justify-start h-auto py-3">
<div className="p-2 rounded-lg bg-orange-500/15 mr-3 shrink-0">
<AlertTriangle className="w-4 h-4 text-orange-500" />
</div>
<div className="text-left min-w-0">
<p className="font-medium text-sm truncate">High Priority</p>
<p className="text-xs text-muted-foreground truncate">View urgent tasks</p>
<p className="text-xs text-muted-foreground truncate">Urgent/high in current sprint</p>
</div>
</Button>
</Link>
<Link href={getGanttTasksUrl({ status: "in-progress" })} target="_blank" className="block">
<Link
href={getGanttTasksUrl({ status: "in-progress", scope: "active-sprint" })}
target="_blank"
className="block"
>
<Button variant="outline" className="w-full justify-start h-auto py-3">
<div className="p-2 rounded-lg bg-green-500/15 mr-3 shrink-0">
<CheckCircle2 className="w-4 h-4 text-green-500" />
</div>
<div className="text-left min-w-0">
<p className="font-medium text-sm truncate">In Progress</p>
<p className="text-xs text-muted-foreground truncate">Active tasks</p>
<p className="text-xs text-muted-foreground truncate">Current sprint in-progress tasks</p>
</div>
</Button>
</Link>

View File

@ -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<CapGate[]> {
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<CapGate | null> {
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<boolean> {
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<CapGateCheckResult> {
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<CapGateCheck> {
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<CapGateCheckResult> {
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<boolean> {
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<CapGate, 'id' | 'created_at' | 'updated_at'>[] = [
{
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<boolean> {
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;
}

View File

@ -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<DailyMissionGenerationResult | null> {
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<string, number> = {
'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<CalendarBlock | null> {
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<CalendarBlock, 'id' | 'created_at' | 'updated_at'> = {
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<DailyMission> {
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<DailyMission, 'id' | 'created_at' | 'updated_at'> = {
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<string> {
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<string, unknown>;
errorMessage?: string;
}
): Promise<void> {
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<DailyMission | null> {
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<boolean> {
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<DailyMissionGenerationResult | null> {
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<boolean> {
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<boolean> {
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<boolean> {
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;
}

View File

@ -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<string, number> = {
'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<string, number> = {
'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<CapGateCheckResult> {
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<MissionProposal | null> {
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<MissionProposal[]> {
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<boolean> {
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<boolean> {
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<boolean> {
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<MissionProposal[]> {
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<number> {
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;
}

View File

@ -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<TriggerRule[]> {
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<TriggerEvaluationResult[]> {
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<TriggerEvaluationResult> {
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<string, unknown>
): 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<string, unknown>
): 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<string, unknown>
): 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<string, unknown>
): 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<string, unknown>
): 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<string, unknown>
): 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<string, unknown>
): 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<boolean> {
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<boolean> {
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<void> {
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<void> {
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<void> {
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<TriggerEvaluationResult[]> {
// 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<TriggerLog[]> {
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 || [];
}

View File

@ -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();

440
lib/supabase/voxyz.types.ts Normal file
View File

@ -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<string, unknown>;
rules: Record<string, unknown>;
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<string, unknown>;
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<string, unknown>;
}
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<string, unknown>;
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<string, unknown>;
output_summary: Record<string, unknown>;
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<MissionProposal, 'id' | 'created_at' | 'updated_at'>;
export type PolicyInsert = Omit<Policy, 'id' | 'created_at' | 'updated_at' | 'version'>;
export type TriggerRuleInsert = Omit<TriggerRule, 'id' | 'created_at' | 'updated_at' | 'trigger_count'>;
export type CapGateInsert = Omit<CapGate, 'id' | 'created_at' | 'updated_at' | 'current_value'>;
export type DailyMissionInsert = Omit<DailyMission, 'id' | 'created_at' | 'updated_at'>;
export type CalendarBlockInsert = Omit<CalendarBlock, 'id' | 'created_at' | 'updated_at'>;
// ============================================
// 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<string, unknown>;
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<MissionProposalInsert>;
};
ops_policy: {
Row: Policy;
Insert: PolicyInsert;
Update: Partial<PolicyInsert>;
};
ops_trigger_rules: {
Row: TriggerRule;
Insert: TriggerRuleInsert;
Update: Partial<TriggerRuleInsert>;
};
ops_cap_gates: {
Row: CapGate;
Insert: CapGateInsert;
Update: Partial<CapGateInsert>;
};
ops_daily_missions: {
Row: DailyMission;
Insert: DailyMissionInsert;
Update: Partial<DailyMissionInsert>;
};
ops_calendar_blocks: {
Row: CalendarBlock;
Insert: CalendarBlockInsert;
Update: Partial<CalendarBlockInsert>;
};
ops_trigger_log: {
Row: TriggerLog;
Insert: Omit<TriggerLog, 'id' | 'created_at'>;
Update: Partial<Omit<TriggerLog, 'id' | 'created_at'>>;
};
ops_agent_execution_log: {
Row: AgentExecutionLog;
Insert: Omit<AgentExecutionLog, 'id' | 'created_at'>;
Update: Partial<Omit<AgentExecutionLog, 'id' | 'created_at'>>;
};
};
};
}