// Hermes runs as two co-located instances ("Vijay" on the root host, "Bheem" // under the `uma` user). Every entity that flows through Mission Control — // tasks, products, events, runs, agents — is tagged with the instance that // owns it so panes can filter or roll up across instances. The literal `'all'` // is a UI-only filter value (never stored on entities). export type HermesInstanceId = 'vijay' | 'bheem'; export type HermesInstanceFilter = 'all' | HermesInstanceId; export const HERMES_INSTANCES: ReadonlyArray<{ id: HermesInstanceId; label: string; description: string; }> = [ { id: 'vijay', label: 'Vijay (root)', description: 'Primary host instance' }, { id: 'bheem', label: 'Bheem (uma)', description: 'Secondary user instance' }, ]; export type HermesStatus = 'running' | 'idle' | 'degraded' | 'error'; export type HermesTaskStatus = | 'queued' | 'running' | 'blocked' | 'completed' | 'failed' | 'skipped' | 'cancelled'; export type HermesPriority = 'P0' | 'P1' | 'P2' | 'P3'; export type HermesTaskType = | 'build' | 'deploy' | 'bugfix' | 'monitoring' | 'audit' | 'refactor' | 'documentation' | 'research' | 'security' | 'cost-optimization' | 'release' | 'maintenance' | 'product-planning'; export type HermesTaskSource = | 'manual' | 'cron' | 'github' | 'monitoring-alert' | 'email' | 'cli' | 'webhook' | 'local-agent' | 'hermes-planner'; export interface HermesProduct { id: string; instanceId: HermesInstanceId; name: string; slug: string; description: string; category: string; repoUrl?: string; productionUrl?: string; stagingUrl?: string; owner: string; priority: HermesPriority; status: 'active' | 'paused' | 'maintenance' | 'archived' | 'idea'; healthScore: number; tags: string[]; lastHermesActivityAt?: string; lastDeploymentAt?: string; lastCommitAt?: string; needsAttention: boolean; createdAt: string; updatedAt: string; } export interface HermesTask { id: string; instanceId: HermesInstanceId; title: string; description: string; productId: string; status: HermesTaskStatus; priority: HermesPriority; type: HermesTaskType; source: HermesTaskSource; createdAt: string; startedAt?: string; completedAt?: string; durationMs?: number; retryCount: number; assignedAgent: string; tags: string[]; progressPercent: number; currentStep?: string; lastAction?: string; nextAction?: string; blockerReason?: string; summary?: string; result?: string; error?: string; } export interface HermesEvent { id: string; instanceId: HermesInstanceId; taskId: string; timestamp: string; level: 'debug' | 'info' | 'warn' | 'error' | 'success'; eventType: | 'created' | 'planned' | 'started' | 'tool-called' | 'command-executed' | 'file-changed' | 'test-run' | 'error' | 'retry' | 'blocked' | 'completed' | 'deployment' | 'pr-created' | 'memory-suggested'; message: string; metadata?: Record; toolName?: string; command?: string; artifactUrl?: string; } export interface HermesRun { id: string; instanceId: HermesInstanceId; taskId: string; startedAt: string; endedAt?: string; status: HermesTaskStatus; logs: string[]; metrics?: Record; commitSha?: string; branchName?: string; prUrl?: string; deploymentUrl?: string; } export interface HermesAgentStatus { id: string; // An agent's "scope" — which Hermes instance(s) it serves. Some integrations // (Hermes Core, GitHub link) span both; we model that with a literal `'all'` // rather than duplicating rows. Filtering treats `'all'` as a match for any // selected instance. instanceId: HermesInstanceId | 'all'; name: string; type: 'agent' | 'tool' | 'integration' | 'runner'; status: 'healthy' | 'degraded' | 'offline' | 'unknown'; lastSuccessAt?: string; lastFailureAt?: string; callsToday: number; failureRate: number; averageLatencyMs?: number; configIssue?: string; } export interface HermesOverview { status: HermesStatus; activeTasks: number; completedToday: number; completedThisWeek: number; failedTasks: number; blockedTasks: number; averageDurationMs: number; successRate: number; productsTouchedRecently: number; founderAttentionCount: number; upcomingJobs: number; lastAction: string; nextRecommendedAction: string; } export interface HermesHistoryPoint { label: string; completed: number; failed: number; blocked: number; active: number; } export interface HermesSettings { demoMode: boolean; retentionDays: number; approvalThreshold: number; autoRetryLimit: number; notificationRules: Array<{ id: string; label: string; enabled: boolean; target: string; }>; taskCategories: string[]; priorityRules: Array<{ priority: HermesPriority; rule: string; }>; registry: Array<{ id: string; name: string; enabled: boolean; }>; } const now = Date.now(); const isoMinutesAgo = (minutes: number) => new Date(now - minutes * 60_000).toISOString(); const isoHoursAgo = (hours: number) => new Date(now - hours * 3_600_000).toISOString(); const isoDaysAgo = (days: number) => new Date(now - days * 86_400_000).toISOString(); const productSeeds = [ { name: 'Automation Hub', category: 'automation', owner: 'Hermes', tags: ['workflow', 'cron', 'ops'] }, { name: 'Trading Console', category: 'SaaS', owner: 'Saravana', tags: ['deploy', 'reliability', 'money'] }, { name: 'AI Content Studio', category: 'AI app', owner: 'Hermes', tags: ['llm', 'content', 'creative'] }, { name: 'Internal Ops Desk', category: 'internal tool', owner: 'Platform', tags: ['admin', 'support', 'workflow'] }, { name: 'Marketing Site', category: 'website', owner: 'Growth', tags: ['brand', 'web', 'seo'] }, { name: 'Customer API', category: 'API', owner: 'Engineering', tags: ['api', 'integration', 'platform'] }, { name: 'Agent Runner', category: 'automation', owner: 'Hermes', tags: ['agents', 'cli', 'background'] }, { name: 'Browser Extension', category: 'browser extension', owner: 'Product', tags: ['extension', 'browser', 'ux'] }, { name: 'Analytics Pipeline', category: 'data pipeline', owner: 'Data', tags: ['etl', 'data', 'metrics'] }, { name: 'DevOps Toolkit', category: 'DevOps tool', owner: 'Saravana', tags: ['devops', 'scripts', 'gitea'] }, ]; const priorityCycle: HermesPriority[] = ['P0', 'P1', 'P2', 'P3']; const statusCycle: HermesProduct['status'][] = ['active', 'active', 'maintenance', 'paused', 'active', 'active', 'idea', 'archived']; const taskStatusCycle: HermesTaskStatus[] = ['running', 'queued', 'blocked', 'completed', 'failed', 'completed', 'queued', 'running']; const taskTypeCycle: HermesTaskType[] = [ 'build', 'deploy', 'bugfix', 'monitoring', 'audit', 'refactor', 'documentation', 'research', 'security', 'cost-optimization', 'release', 'maintenance', 'product-planning', ]; const sourceCycle: HermesTaskSource[] = [ 'manual', 'cron', 'github', 'monitoring-alert', 'email', 'cli', 'webhook', 'local-agent', 'hermes-planner', ]; export const hermesProducts: HermesProduct[] = Array.from({ length: 50 }, (_, index) => { const seed = productSeeds[index % productSeeds.length]; const ordinal = index + 1; const status = statusCycle[index % statusCycle.length]; const priority = priorityCycle[index % priorityCycle.length]; const activityAgeDays = (index % 18) + (status === 'active' ? 0 : 7); const deploymentAgeDays = (index % 12) + (status === 'active' ? 1 : 14); const commitAgeDays = (index % 9) + (status === 'active' ? 0 : 10); const healthScore = Math.max( 32, Math.min( 99, 94 - (index % 7) * 4 - (status === 'paused' ? 18 : 0) - (status === 'archived' ? 24 : 0) - (status === 'maintenance' ? 10 : 0) + (priority === 'P0' ? 4 : 0), ), ); // Deterministic split: every other product to Bheem (uma), the rest to // Vijay (root). Gives a roughly 50/50 mix so the switcher is exercisable // out of the box without any single instance going empty. const instanceId: HermesInstanceId = index % 2 === 0 ? 'vijay' : 'bheem'; return { id: `product-${ordinal}`, instanceId, name: `${seed.name} ${ordinal}`, slug: `${seed.name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-${ordinal}`, description: `${seed.category} product managed by Hermes for ${seed.owner.toLowerCase()} workflows.`, category: seed.category, repoUrl: `https://github.com/bytelyst/${seed.name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-${ordinal}`, productionUrl: status === 'archived' ? undefined : `https://${seed.name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-${ordinal}.bytelyst.ai`, stagingUrl: status === 'idea' ? undefined : `https://staging-${seed.name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-${ordinal}.bytelyst.ai`, owner: seed.owner, priority, status, healthScore, tags: seed.tags, lastHermesActivityAt: isoDaysAgo(activityAgeDays), lastDeploymentAt: status === 'idea' ? undefined : isoDaysAgo(deploymentAgeDays), lastCommitAt: isoDaysAgo(commitAgeDays), needsAttention: healthScore < 72 || status !== 'active', createdAt: isoDaysAgo(60 + index), updatedAt: isoDaysAgo(index % 10), }; }); const taskTemplates = [ 'stabilize deployment pipeline', 'investigate retry loop', 'ship dashboard enhancement', 'review CI failure cluster', 'refactor service integration', 'document product launch flow', 'audit secrets and rotation', 'prepare release checklist', 'benchmark response latency', 'resolve blocked automation', 'ship product telemetry update', 'clean up stale jobs', ]; const assignedAgents = ['Hermes Core', 'OpenClaw', 'Gitea Bot', 'Local VM Runner', 'Planner', 'Notifier']; export const hermesTasks: HermesTask[] = Array.from({ length: 36 }, (_, index) => { const product = hermesProducts[index % hermesProducts.length]; const status = taskStatusCycle[index % taskStatusCycle.length]; const priority = priorityCycle[(index + (status === 'blocked' ? 0 : 1)) % priorityCycle.length]; const type = taskTypeCycle[index % taskTypeCycle.length]; const source = sourceCycle[index % sourceCycle.length]; const createdHoursAgo = index * 3 + 4; const startedHoursAgo = createdHoursAgo - 1; const durationMinutes = 16 + (index % 6) * 9; const title = `${taskTemplates[index % taskTemplates.length]} — ${product.name}`; const blockerReason = status === 'blocked' ? ['Waiting on approval', 'Missing credential', 'Test failure needs triage', 'Deployment gate is red'][index % 4] : status === 'failed' ? ['Flaky integration test', 'Build timeout', 'Blocked by external dependency'][index % 3] : undefined; const result = status === 'completed' ? ['Merged and deployed', 'Doc updated and published', 'Audit completed, no action needed', 'PR opened for review'][index % 4] : undefined; const error = status === 'failed' ? ['npm test exited 1', 'upstream service returned 500', 'timeout waiting for health check'][index % 3] : undefined; const progressPercent = status === 'completed' ? 100 : status === 'running' ? 55 + (index % 20) : status === 'queued' ? 10 + (index % 15) : status === 'blocked' ? 35 : status === 'failed' ? 48 : status === 'skipped' ? 100 : 0; return { id: `task-${index + 1}`, instanceId: product.instanceId, title, description: `Hermes is working on ${taskTemplates[index % taskTemplates.length]} for ${product.name}.`, productId: product.id, status, priority, type, source, createdAt: isoHoursAgo(createdHoursAgo), startedAt: status === 'queued' ? undefined : isoHoursAgo(startedHoursAgo), completedAt: status === 'completed' || status === 'skipped' ? isoHoursAgo(Math.max(0, startedHoursAgo - 1)) : undefined, durationMs: status === 'completed' || status === 'failed' || status === 'skipped' ? durationMinutes * 60_000 : undefined, retryCount: index % 4, assignedAgent: assignedAgents[index % assignedAgents.length], tags: [type, priority.toLowerCase(), product.category.toLowerCase()], progressPercent, currentStep: status === 'running' ? ['reviewing logs', 'applying patch', 'replaying checks', 'waiting for approval'][index % 4] : undefined, lastAction: ['Checked repo state', 'Ran typecheck', 'Updated docs', 'Pushed branch', 'Re-ran tests'][index % 5], nextAction: status === 'completed' ? 'Monitor telemetry' : status === 'blocked' ? 'Await founder decision' : status === 'failed' ? 'Investigate failure and retry' : 'Continue task execution', blockerReason, summary: status === 'completed' ? 'Work completed successfully with evidence captured.' : 'Active orchestration task tracked by Hermes.', result, error, }; }); const eventBlueprints = new Map( hermesTasks.map((task, index) => { const created = isoHoursAgo(index * 3 + 4); const started = isoHoursAgo(index * 3 + 3); const completed = isoHoursAgo(index * 3 + 1); const events: HermesEvent[] = [ { id: `${task.id}-event-created`, instanceId: task.instanceId, taskId: task.id, timestamp: created, level: 'info', eventType: 'created', message: `Task ${task.title} was created from ${task.source}.`, metadata: { productId: task.productId, priority: task.priority }, }, { id: `${task.id}-event-planned`, instanceId: task.instanceId, taskId: task.id, timestamp: started, level: 'info', eventType: 'planned', message: `Hermes planned the next steps for ${task.title}.`, }, ]; if (task.status === 'running' || task.status === 'completed' || task.status === 'blocked' || task.status === 'failed') { events.push({ id: `${task.id}-event-started`, instanceId: task.instanceId, taskId: task.id, timestamp: started, level: 'success', eventType: 'started', message: `${task.assignedAgent} started execution.`, }); events.push({ id: `${task.id}-event-command`, instanceId: task.instanceId, taskId: task.id, timestamp: isoMinutesAgo(index * 7 + 25), level: 'debug', eventType: 'command-executed', message: 'Ran the verification command set.', command: 'pnpm test:run && pnpm build', toolName: 'terminal', }); } if (task.status === 'blocked') { events.push({ id: `${task.id}-event-blocked`, instanceId: task.instanceId, taskId: task.id, timestamp: isoMinutesAgo(index * 7 + 10), level: 'warn', eventType: 'blocked', message: task.blockerReason ?? 'Task blocked pending review.', metadata: { attentionRequired: true }, }); } if (task.status === 'failed') { events.push({ id: `${task.id}-event-error`, instanceId: task.instanceId, taskId: task.id, timestamp: isoMinutesAgo(index * 7 + 8), level: 'error', eventType: 'error', message: task.error ?? 'Execution failed.', metadata: { retryCount: task.retryCount }, }); events.push({ id: `${task.id}-event-retry`, instanceId: task.instanceId, taskId: task.id, timestamp: isoMinutesAgo(index * 7 + 5), level: 'warn', eventType: 'retry', message: 'Hermes scheduled an automatic retry.', }); } if (task.status === 'completed' || task.status === 'skipped') { events.push({ id: `${task.id}-event-completed`, instanceId: task.instanceId, taskId: task.id, timestamp: completed, level: 'success', eventType: 'completed', message: task.result ?? 'Task completed successfully.', artifactUrl: `https://github.com/bytelyst/hermes/${task.id}`, }); if (task.type === 'deploy') { events.push({ id: `${task.id}-event-deployment`, instanceId: task.instanceId, taskId: task.id, timestamp: completed, level: 'success', eventType: 'deployment', message: 'Deployment finished and health check passed.', }); } } if (task.priority === 'P0') { events.push({ id: `${task.id}-event-memory`, instanceId: task.instanceId, taskId: task.id, timestamp: isoMinutesAgo(index * 7 + 2), level: 'info', eventType: 'memory-suggested', message: 'Hermes suggested a follow-up memory entry to prevent repeat failures.', }); } return [task.id, events]; }), ); export const hermesAgentStatuses: HermesAgentStatus[] = [ { id: 'hermes-core', instanceId: 'all', name: 'Hermes Core', type: 'agent', status: 'healthy', lastSuccessAt: isoHoursAgo(1), callsToday: 42, failureRate: 0.03, averageLatencyMs: 820, }, { id: 'openclaw-integration', instanceId: 'all', name: 'OpenClaw integration', type: 'integration', status: 'degraded', lastSuccessAt: isoHoursAgo(5), lastFailureAt: isoHoursAgo(1), callsToday: 11, failureRate: 0.18, averageLatencyMs: 1480, configIssue: 'Rate-limit warnings from the upstream workspace token.', }, { id: 'github-link', instanceId: 'all', name: 'GitHub integration', type: 'integration', status: 'healthy', lastSuccessAt: isoHoursAgo(2), callsToday: 84, failureRate: 0.01, averageLatencyMs: 420, }, { id: 'local-vm-runner', instanceId: 'bheem', name: 'Local VM runner', type: 'runner', status: 'healthy', lastSuccessAt: isoMinutesAgo(18), callsToday: 27, failureRate: 0.02, averageLatencyMs: 670, }, { id: 'cli-runner', instanceId: 'vijay', name: 'CLI runner', type: 'runner', status: 'healthy', lastSuccessAt: isoMinutesAgo(6), callsToday: 33, failureRate: 0.02, averageLatencyMs: 510, }, { id: 'scheduler-cron', instanceId: 'vijay', name: 'Scheduler / cron', type: 'tool', status: 'healthy', lastSuccessAt: isoMinutesAgo(9), callsToday: 67, failureRate: 0.00, averageLatencyMs: 112, }, { id: 'deployment-tools', instanceId: 'all', name: 'Deployment tools', type: 'tool', status: 'degraded', lastSuccessAt: isoHoursAgo(3), lastFailureAt: isoHoursAgo(1), callsToday: 19, failureRate: 0.11, averageLatencyMs: 900, configIssue: 'One stale secret needs rotation before the next release.', }, { id: 'notifications', instanceId: 'all', name: 'Notification tools', type: 'tool', status: 'offline', lastSuccessAt: isoDaysAgo(2), lastFailureAt: isoHoursAgo(9), callsToday: 4, failureRate: 0.25, averageLatencyMs: 2100, configIssue: 'Telegram webhook token not configured in the mock environment.', }, ]; export const hermesHistory: HermesHistoryPoint[] = [ { label: 'Wk 1', completed: 12, failed: 2, blocked: 1, active: 4 }, { label: 'Wk 2', completed: 18, failed: 1, blocked: 2, active: 5 }, { label: 'Wk 3', completed: 15, failed: 4, blocked: 2, active: 6 }, { label: 'Wk 4', completed: 21, failed: 3, blocked: 1, active: 7 }, { label: 'Wk 5', completed: 17, failed: 2, blocked: 3, active: 6 }, { label: 'Wk 6', completed: 24, failed: 1, blocked: 1, active: 5 }, { label: 'Wk 7', completed: 20, failed: 2, blocked: 2, active: 6 }, { label: 'Wk 8', completed: 26, failed: 1, blocked: 0, active: 4 }, ]; export const hermesSettings: HermesSettings = { demoMode: true, retentionDays: 45, approvalThreshold: 75, autoRetryLimit: 2, notificationRules: [ { id: 'approval-needed', label: 'Approval needed', enabled: true, target: 'Telegram + dashboard badge' }, { id: 'deploy-failure', label: 'Failed deployment', enabled: true, target: 'Telegram' }, { id: 'repeated-failure', label: 'Repeated failures', enabled: true, target: 'Email digest' }, { id: 'cost-risk', label: 'Cost or risk warning', enabled: false, target: 'Founder review queue' }, ], taskCategories: [ 'build', 'deploy', 'bugfix', 'monitoring', 'audit', 'refactor', 'documentation', 'research', 'security', 'cost-optimization', 'release', 'maintenance', 'product-planning', ], priorityRules: [ { priority: 'P0', rule: 'Production incidents, blocked launches, or security issues.' }, { priority: 'P1', rule: 'High-value shipping work with founder impact.' }, { priority: 'P2', rule: 'Normal operating work and maintenance.' }, { priority: 'P3', rule: 'Nice-to-have improvements and backlog hygiene.' }, ], registry: [ { id: 'hermes-core', name: 'Hermes Core', enabled: true }, { id: 'github', name: 'GitHub', enabled: true }, { id: 'local-vm', name: 'Local VM Runner', enabled: true }, { id: 'notifications', name: 'Notifications', enabled: false }, ], }; export interface HermesTaskFilters { query?: string; status?: HermesTaskStatus | 'all'; productId?: string | 'all'; priority?: HermesPriority | 'all'; type?: HermesTaskType | 'all'; source?: HermesTaskSource | 'all'; // Restrict to a single Hermes instance, or roll up across both with `'all'`. instance?: HermesInstanceFilter; updatedWithinDays?: number | 'all'; sort?: 'newest' | 'oldest' | 'priority' | 'status'; } // Helper used by every list-fetcher in this module so the filter semantics // stay consistent: an entity matches when the requested filter is `'all'` OR // equals the entity's own instance. For agents whose own scope is `'all'`, // they always match (they live on both instances). function instanceMatches( entityScope: HermesInstanceId | 'all', filter: HermesInstanceFilter, ): boolean { if (filter === 'all') return true; if (entityScope === 'all') return true; return entityScope === filter; } export function getHermesOverview(instance: HermesInstanceFilter = 'all'): HermesOverview { const tasks = hermesTasks.filter((task) => instanceMatches(task.instanceId, instance)); const products = hermesProducts.filter((product) => instanceMatches(product.instanceId, instance)); const activeTasks = tasks.filter((task) => task.status === 'running' || task.status === 'queued' || task.status === 'blocked').length; const completedToday = tasks.filter((task) => task.completedAt && new Date(task.completedAt).getTime() >= now - 86_400_000).length; const completedThisWeek = tasks.filter((task) => task.completedAt && new Date(task.completedAt).getTime() >= now - 7 * 86_400_000).length; const failedTasks = tasks.filter((task) => task.status === 'failed').length; const blockedTasks = tasks.filter((task) => task.status === 'blocked').length; const completedWithDuration = tasks.filter((task) => typeof task.durationMs === 'number' && task.status === 'completed'); const averageDurationMs = completedWithDuration.length ? Math.round(completedWithDuration.reduce((sum, task) => sum + (task.durationMs ?? 0), 0) / completedWithDuration.length) : 0; const successRate = tasks.length === 0 ? 0 : Math.round((tasks.filter((task) => task.status === 'completed' || task.status === 'skipped').length / tasks.length) * 100); const productsTouchedRecently = products.filter((product) => product.lastHermesActivityAt && new Date(product.lastHermesActivityAt).getTime() >= now - 14 * 86_400_000).length; const founderAttentionCount = tasks.filter((task) => task.status === 'blocked' || task.status === 'failed').length + products.filter((product) => product.needsAttention).length; const upcomingJobs = tasks.filter((task) => task.status === 'queued').length; const lastAction = hermesEventsSorted(instance)[0]?.message ?? 'Hermes has not recorded an action yet.'; const nextRecommendedAction = computeNextRecommendedAction(instance); return { status: failedTasks > 6 ? 'error' : blockedTasks > 4 ? 'degraded' : activeTasks > 0 ? 'running' : 'idle', activeTasks, completedToday, completedThisWeek, failedTasks, blockedTasks, averageDurationMs, successRate, productsTouchedRecently, founderAttentionCount, upcomingJobs, lastAction, nextRecommendedAction, }; } export function getHermesTasks(filters: HermesTaskFilters = {}): HermesTask[] { const { query, status = 'all', productId = 'all', priority = 'all', type = 'all', source = 'all', instance = 'all', updatedWithinDays = 'all', sort = 'newest', } = filters; const normalizedQuery = query?.trim().toLowerCase(); const filtered = hermesTasks.filter((task) => { if (!instanceMatches(task.instanceId, instance)) return false; const product = hermesProducts.find((item) => item.id === task.productId); const matchesQuery = !normalizedQuery || [ task.title, task.description, task.assignedAgent, task.lastAction, task.nextAction, task.blockerReason, task.result, task.error, product?.name, product?.slug, ...task.tags, ] .filter(Boolean) .some((value) => String(value).toLowerCase().includes(normalizedQuery)); const matchesStatus = status === 'all' || task.status === status; const matchesProduct = productId === 'all' || task.productId === productId; const matchesPriority = priority === 'all' || task.priority === priority; const matchesType = type === 'all' || task.type === type; const matchesSource = source === 'all' || task.source === source; const matchesUpdatedWindow = updatedWithinDays === 'all' ? true : new Date(task.createdAt).getTime() >= now - updatedWithinDays * 86_400_000; return matchesQuery && matchesStatus && matchesProduct && matchesPriority && matchesType && matchesSource && matchesUpdatedWindow; }); const priorityRank: Record = { P0: 0, P1: 1, P2: 2, P3: 3 }; return [...filtered].sort((a, b) => { switch (sort) { case 'oldest': return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); case 'priority': return priorityRank[a.priority] - priorityRank[b.priority] || new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); case 'status': return a.status.localeCompare(b.status) || new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); case 'newest': default: return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); } }); } export function getHermesTaskById(id: string): HermesTask | undefined { return hermesTasks.find((task) => task.id === id); } export function getHermesTaskEvents(taskId: string): HermesEvent[] { return eventBlueprints.get(taskId) ?? []; } export function getHermesProductById(id: string): HermesProduct | undefined { return hermesProducts.find((product) => product.id === id); } export function getHermesProducts( view: 'all' | 'high-priority' | 'needs-attention' | 'no-recent-activity' | 'repeated-failures' | 'recently-shipped' = 'all', instance: HermesInstanceFilter = 'all', ): HermesProduct[] { const recentCutoff = now - 14 * 86_400_000; const shippedCutoff = now - 7 * 86_400_000; return hermesProducts.filter((product) => { if (!instanceMatches(product.instanceId, instance)) return false; const recentFailedTasks = hermesTasks.filter((task) => task.productId === product.id && task.status === 'failed').length; const recentCompletedTasks = hermesTasks.filter((task) => task.productId === product.id && task.status === 'completed').length; switch (view) { case 'high-priority': return product.priority === 'P0' || product.priority === 'P1'; case 'needs-attention': return product.needsAttention; case 'no-recent-activity': return !product.lastHermesActivityAt || new Date(product.lastHermesActivityAt).getTime() < recentCutoff; case 'repeated-failures': return recentFailedTasks >= 3; case 'recently-shipped': return recentCompletedTasks > 0 && (product.lastDeploymentAt ? new Date(product.lastDeploymentAt).getTime() >= shippedCutoff : false); case 'all': default: return true; } }); } export function getHermesHistory(instance: HermesInstanceFilter = 'all'): HermesHistoryPoint[] { // History is hand-tuned aggregate seed data. For the per-instance filter we // approximate by halving each bar (instances were seeded ~50/50). This is // mock data — Phase 3 replaces it with real per-instance time series. if (instance === 'all') return hermesHistory; return hermesHistory.map((point) => ({ label: point.label, completed: Math.round(point.completed / 2), failed: Math.round(point.failed / 2), blocked: Math.round(point.blocked / 2), active: Math.round(point.active / 2), })); } export function getHermesAgents(instance: HermesInstanceFilter = 'all'): HermesAgentStatus[] { return hermesAgentStatuses.filter((agent) => instanceMatches(agent.instanceId, instance)); } export function getHermesSettings() { return hermesSettings; } function hermesEventsSorted(instance: HermesInstanceFilter = 'all') { return Array.from(eventBlueprints.values()) .flat() .filter((event) => instanceMatches(event.instanceId, instance)) .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); } function computeNextRecommendedAction(instance: HermesInstanceFilter = 'all') { const tasks = hermesTasks.filter((task) => instanceMatches(task.instanceId, instance)); const products = hermesProducts.filter((product) => instanceMatches(product.instanceId, instance)); const blocked = tasks.filter((task) => task.status === 'blocked'); if (blocked.length > 0) { const next = blocked[0]; return `Unblock ${next.title} for ${getHermesProductById(next.productId)?.name ?? 'an active product'}.`; } const failed = tasks.find((task) => task.status === 'failed'); if (failed) { return `Inspect and retry ${failed.title}.`; } const staleProduct = products.find((product) => product.needsAttention); if (staleProduct) { return `Review ${staleProduct.name} because it needs attention.`; } return 'No urgent action required. Continue the scheduled execution queue.'; }