/** * Headquarters Router * * Provides the composite dashboard query for the Headquarters page, * aggregating all action items that require user intervention. */ import type { ProcedureBuilder } from '../trpc.js'; import type { Phase } from '../../db/schema.js'; import { requireAgentManager, requireInitiativeRepository, requirePhaseRepository, } from './_helpers.js'; export function headquartersProcedures(publicProcedure: ProcedureBuilder) { return { getHeadquartersDashboard: publicProcedure.query(async ({ ctx }) => { const initiativeRepo = requireInitiativeRepository(ctx); const phaseRepo = requirePhaseRepository(ctx); const agentManager = requireAgentManager(ctx); const [allInitiatives, allAgents] = await Promise.all([ initiativeRepo.findAll(), agentManager.list(), ]); // Relevant initiatives: status in ['active', 'pending_review'] const relevantInitiatives = allInitiatives.filter( (i) => i.status === 'active' || i.status === 'pending_review', ); // Non-dismissed agents only const activeAgents = allAgents.filter((a) => !a.userDismissedAt); // Fast lookup map: initiative id → initiative const initiativeMap = new Map(relevantInitiatives.map((i) => [i.id, i])); // Batch-fetch all phases for relevant initiatives in parallel const phasesByInitiative = new Map(); await Promise.all( relevantInitiatives.map(async (init) => { const phases = await phaseRepo.findByInitiativeId(init.id); phasesByInitiative.set(init.id, phases); }), ); // ----------------------------------------------------------------------- // Section 1: waitingForInput // ----------------------------------------------------------------------- const waitingAgents = activeAgents.filter((a) => a.status === 'waiting_for_input'); const pendingQuestionsResults = await Promise.all( waitingAgents.map((a) => agentManager.getPendingQuestions(a.id)), ); const waitingForInput = waitingAgents .map((agent, i) => { const initiative = agent.initiativeId ? initiativeMap.get(agent.initiativeId) : undefined; return { agentId: agent.id, agentName: agent.name, initiativeId: agent.initiativeId, initiativeName: initiative?.name ?? null, questionText: pendingQuestionsResults[i]?.questions[0]?.question ?? '', waitingSince: agent.updatedAt.toISOString(), }; }) .sort((a, b) => a.waitingSince.localeCompare(b.waitingSince)); // ----------------------------------------------------------------------- // Section 2a: pendingReviewInitiatives // ----------------------------------------------------------------------- const pendingReviewInitiatives = relevantInitiatives .filter((i) => i.status === 'pending_review') .map((i) => ({ initiativeId: i.id, initiativeName: i.name, since: i.updatedAt.toISOString(), })) .sort((a, b) => a.since.localeCompare(b.since)); // ----------------------------------------------------------------------- // Section 2b: pendingReviewPhases // ----------------------------------------------------------------------- const pendingReviewPhases: Array<{ initiativeId: string; initiativeName: string; phaseId: string; phaseName: string; since: string; }> = []; for (const [initiativeId, phases] of phasesByInitiative) { const initiative = initiativeMap.get(initiativeId)!; for (const phase of phases) { if (phase.status === 'pending_review') { pendingReviewPhases.push({ initiativeId, initiativeName: initiative.name, phaseId: phase.id, phaseName: phase.name, since: phase.updatedAt.toISOString(), }); } } } pendingReviewPhases.sort((a, b) => a.since.localeCompare(b.since)); // ----------------------------------------------------------------------- // Section 3: planningInitiatives // ----------------------------------------------------------------------- const planningInitiatives: Array<{ initiativeId: string; initiativeName: string; pendingPhaseCount: number; since: string; }> = []; for (const initiative of relevantInitiatives) { if (initiative.status !== 'active') continue; const phases = phasesByInitiative.get(initiative.id) ?? []; if (phases.length === 0) continue; const allPending = phases.every((p) => p.status === 'pending'); if (!allPending) continue; const hasActiveAgent = activeAgents.some( (a) => a.initiativeId === initiative.id && (a.status === 'running' || a.status === 'waiting_for_input'), ); if (hasActiveAgent) continue; const sortedByCreatedAt = [...phases].sort( (a, b) => a.createdAt.getTime() - b.createdAt.getTime(), ); planningInitiatives.push({ initiativeId: initiative.id, initiativeName: initiative.name, pendingPhaseCount: phases.length, since: sortedByCreatedAt[0].createdAt.toISOString(), }); } planningInitiatives.sort((a, b) => a.since.localeCompare(b.since)); // ----------------------------------------------------------------------- // Section 4: resolvingConflicts // ----------------------------------------------------------------------- const resolvingConflicts: Array<{ initiativeId: string; initiativeName: string; agentId: string; agentName: string; agentStatus: string; since: string; }> = []; for (const agent of activeAgents) { if ( agent.name?.startsWith('conflict-') && (agent.status === 'running' || agent.status === 'waiting_for_input') && agent.initiativeId ) { const initiative = initiativeMap.get(agent.initiativeId); if (initiative) { resolvingConflicts.push({ initiativeId: initiative.id, initiativeName: initiative.name, agentId: agent.id, agentName: agent.name, agentStatus: agent.status, since: agent.updatedAt.toISOString(), }); } } } resolvingConflicts.sort((a, b) => a.since.localeCompare(b.since)); // ----------------------------------------------------------------------- // Section 5: blockedPhases // ----------------------------------------------------------------------- const blockedPhases: Array<{ initiativeId: string; initiativeName: string; phaseId: string; phaseName: string; lastMessage: string | null; since: string; }> = []; for (const initiative of relevantInitiatives) { if (initiative.status !== 'active') continue; const phases = phasesByInitiative.get(initiative.id) ?? []; for (const phase of phases) { if (phase.status !== 'blocked') continue; let lastMessage: string | null = null; try { if (ctx.taskRepository && ctx.messageRepository) { const taskRepo = ctx.taskRepository; const messageRepo = ctx.messageRepository; const tasks = await taskRepo.findByPhaseId(phase.id); const phaseAgentIds = allAgents .filter((a) => tasks.some((t) => t.id === a.taskId)) .map((a) => a.id); if (phaseAgentIds.length > 0) { const messageLists = await Promise.all( phaseAgentIds.map((id) => messageRepo.findBySender('agent', id)), ); const allMessages = messageLists .flat() .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); if (allMessages.length > 0) { lastMessage = allMessages[0].content.slice(0, 160); } } } } catch { // Non-critical: message retrieval failure does not crash the dashboard } blockedPhases.push({ initiativeId: initiative.id, initiativeName: initiative.name, phaseId: phase.id, phaseName: phase.name, lastMessage, since: phase.updatedAt.toISOString(), }); } } blockedPhases.sort((a, b) => a.since.localeCompare(b.since)); return { waitingForInput, pendingReviewInitiatives, pendingReviewPhases, planningInitiatives, resolvingConflicts, blockedPhases, }; }), }; }