Signed-off-by: OpenClaw Bot <ai-agent@topdoglabs.com>
This commit is contained in:
parent
0092b318c2
commit
59f85de572
@ -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)
|
||||
|
||||
151
app/api/voxyz/daily-missions/route.ts
Normal file
151
app/api/voxyz/daily-missions/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
189
app/api/voxyz/proposals/route.ts
Normal file
189
app/api/voxyz/proposals/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
577
lib/services/cap-gates.service.ts
Normal file
577
lib/services/cap-gates.service.ts
Normal 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;
|
||||
}
|
||||
642
lib/services/daily-mission-agent.ts
Normal file
642
lib/services/daily-mission-agent.ts
Normal 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;
|
||||
}
|
||||
737
lib/services/proposal.service.ts
Normal file
737
lib/services/proposal.service.ts
Normal 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;
|
||||
}
|
||||
587
lib/services/trigger-rules.service.ts
Normal file
587
lib/services/trigger-rules.service.ts
Normal 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 || [];
|
||||
}
|
||||
504
lib/supabase/migrations/001_voxyz_phase10_schema.sql
Normal file
504
lib/supabase/migrations/001_voxyz_phase10_schema.sql
Normal 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
440
lib/supabase/voxyz.types.ts
Normal 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'>>;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user