@stackbilt/aegis-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/package.json +96 -0
  2. package/schema.sql +586 -0
  3. package/src/adapters/voice/cloudflare-agent.ts +34 -0
  4. package/src/auth.ts +124 -0
  5. package/src/bluesky.ts +464 -0
  6. package/src/claude-tools/content.ts +188 -0
  7. package/src/claude-tools/email.ts +69 -0
  8. package/src/claude-tools/github.ts +440 -0
  9. package/src/claude-tools/goals.ts +116 -0
  10. package/src/claude-tools/index.ts +353 -0
  11. package/src/claude-tools/web.ts +59 -0
  12. package/src/claude.ts +406 -0
  13. package/src/codebeast.ts +200 -0
  14. package/src/composite.ts +715 -0
  15. package/src/content/column.ts +80 -0
  16. package/src/content/hero-image.ts +47 -0
  17. package/src/content/index.ts +27 -0
  18. package/src/content/journal.ts +91 -0
  19. package/src/content/roundtable.ts +163 -0
  20. package/src/core.ts +309 -0
  21. package/src/dashboard.ts +620 -0
  22. package/src/decision-docs.ts +284 -0
  23. package/src/dispatch.ts +13 -0
  24. package/src/edge-env.ts +58 -0
  25. package/src/email.ts +850 -0
  26. package/src/exports.ts +156 -0
  27. package/src/github-projects.ts +312 -0
  28. package/src/github.ts +670 -0
  29. package/src/groq.ts +247 -0
  30. package/src/health-page.ts +578 -0
  31. package/src/index.ts +89 -0
  32. package/src/kernel/argus-actions.ts +397 -0
  33. package/src/kernel/argus-correlation.ts +639 -0
  34. package/src/kernel/board.ts +91 -0
  35. package/src/kernel/briefing.ts +177 -0
  36. package/src/kernel/classify-memory-topic.ts +166 -0
  37. package/src/kernel/cognition.ts +377 -0
  38. package/src/kernel/court-cards.ts +163 -0
  39. package/src/kernel/dispatch.ts +587 -0
  40. package/src/kernel/domain.ts +50 -0
  41. package/src/kernel/dynamic-tools.ts +322 -0
  42. package/src/kernel/executor-port.ts +45 -0
  43. package/src/kernel/executors/claude.ts +73 -0
  44. package/src/kernel/executors/direct.ts +237 -0
  45. package/src/kernel/executors/groq.ts +18 -0
  46. package/src/kernel/executors/index.ts +87 -0
  47. package/src/kernel/executors/tarotscript.ts +104 -0
  48. package/src/kernel/executors/workers-ai.ts +54 -0
  49. package/src/kernel/insight-cache.ts +76 -0
  50. package/src/kernel/memory/agenda.ts +200 -0
  51. package/src/kernel/memory/blocks.ts +188 -0
  52. package/src/kernel/memory/consolidation.ts +194 -0
  53. package/src/kernel/memory/episodic.ts +241 -0
  54. package/src/kernel/memory/goals.ts +156 -0
  55. package/src/kernel/memory/graph.ts +290 -0
  56. package/src/kernel/memory/index.ts +11 -0
  57. package/src/kernel/memory/insights.ts +316 -0
  58. package/src/kernel/memory/procedural.ts +467 -0
  59. package/src/kernel/memory/pruning.ts +67 -0
  60. package/src/kernel/memory/recall.ts +367 -0
  61. package/src/kernel/memory/semantic.ts +315 -0
  62. package/src/kernel/memory/synthesis.ts +161 -0
  63. package/src/kernel/memory-adapter.ts +369 -0
  64. package/src/kernel/memory-guardrails.ts +76 -0
  65. package/src/kernel/port.ts +23 -0
  66. package/src/kernel/resilience.ts +322 -0
  67. package/src/kernel/router.ts +471 -0
  68. package/src/kernel/scheduled/agent-dispatch.ts +252 -0
  69. package/src/kernel/scheduled/argus-analytics.ts +247 -0
  70. package/src/kernel/scheduled/argus-heartbeat.ts +320 -0
  71. package/src/kernel/scheduled/argus-notify.ts +348 -0
  72. package/src/kernel/scheduled/board-sync.ts +110 -0
  73. package/src/kernel/scheduled/ci-watcher.ts +125 -0
  74. package/src/kernel/scheduled/cognitive-metrics.ts +377 -0
  75. package/src/kernel/scheduled/consolidation.ts +229 -0
  76. package/src/kernel/scheduled/content-drip.ts +47 -0
  77. package/src/kernel/scheduled/content.ts +6 -0
  78. package/src/kernel/scheduled/conversation-facts.ts +204 -0
  79. package/src/kernel/scheduled/cost-report.ts +84 -0
  80. package/src/kernel/scheduled/curiosity.ts +219 -0
  81. package/src/kernel/scheduled/dev-activity.ts +44 -0
  82. package/src/kernel/scheduled/digest.ts +317 -0
  83. package/src/kernel/scheduled/dreaming/agenda-triage.ts +115 -0
  84. package/src/kernel/scheduled/dreaming/facts.ts +239 -0
  85. package/src/kernel/scheduled/dreaming/index.ts +8 -0
  86. package/src/kernel/scheduled/dreaming/llm.ts +33 -0
  87. package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +124 -0
  88. package/src/kernel/scheduled/dreaming/persona.ts +75 -0
  89. package/src/kernel/scheduled/dreaming/symbolic.ts +31 -0
  90. package/src/kernel/scheduled/dreaming/task-proposals.ts +80 -0
  91. package/src/kernel/scheduled/dreaming.ts +66 -0
  92. package/src/kernel/scheduled/entropy.ts +149 -0
  93. package/src/kernel/scheduled/escalation.ts +192 -0
  94. package/src/kernel/scheduled/feed-watcher.ts +206 -0
  95. package/src/kernel/scheduled/goals.ts +214 -0
  96. package/src/kernel/scheduled/governance.ts +41 -0
  97. package/src/kernel/scheduled/heartbeat.ts +220 -0
  98. package/src/kernel/scheduled/inbox-processor.ts +174 -0
  99. package/src/kernel/scheduled/index.ts +245 -0
  100. package/src/kernel/scheduled/issue-proposer.ts +478 -0
  101. package/src/kernel/scheduled/issue-watcher.ts +128 -0
  102. package/src/kernel/scheduled/pr-automerge.ts +213 -0
  103. package/src/kernel/scheduled/product-health.ts +107 -0
  104. package/src/kernel/scheduled/reflection.ts +373 -0
  105. package/src/kernel/scheduled/self-improvement.ts +114 -0
  106. package/src/kernel/scheduled/social-engage.ts +175 -0
  107. package/src/kernel/scheduled/task-audit.ts +60 -0
  108. package/src/kernel/symbolic.ts +156 -0
  109. package/src/kernel/types.ts +145 -0
  110. package/src/landing.ts +1190 -0
  111. package/src/lib/audit-chain/chain.ts +28 -0
  112. package/src/lib/audit-chain/types.ts +12 -0
  113. package/src/lib/observability/errors.ts +55 -0
  114. package/src/markdown.ts +164 -0
  115. package/src/mcp/handlers.ts +647 -0
  116. package/src/mcp/server.ts +184 -0
  117. package/src/mcp/tools.ts +316 -0
  118. package/src/mcp-client.ts +275 -0
  119. package/src/mcp-server.ts +2 -0
  120. package/src/operator/config.example.ts +60 -0
  121. package/src/operator/config.ts +60 -0
  122. package/src/operator/index.ts +46 -0
  123. package/src/operator/persona.example.ts +34 -0
  124. package/src/operator/persona.ts +34 -0
  125. package/src/operator/prompt-builder.ts +190 -0
  126. package/src/operator/types.ts +43 -0
  127. package/src/pulse.ts +1179 -0
  128. package/src/routes/bluesky.ts +116 -0
  129. package/src/routes/cc-tasks.ts +328 -0
  130. package/src/routes/codebeast.ts +1 -0
  131. package/src/routes/content.ts +194 -0
  132. package/src/routes/conversations.ts +25 -0
  133. package/src/routes/dynamic-tools.ts +111 -0
  134. package/src/routes/feedback.ts +192 -0
  135. package/src/routes/health.ts +147 -0
  136. package/src/routes/messages.ts +228 -0
  137. package/src/routes/observability.ts +82 -0
  138. package/src/routes/operator-logs.ts +42 -0
  139. package/src/routes/pages.ts +96 -0
  140. package/src/routes/sessions.ts +54 -0
  141. package/src/sanitize.ts +73 -0
  142. package/src/schema-enums.ts +155 -0
  143. package/src/search.ts +112 -0
  144. package/src/task-intelligence.ts +497 -0
  145. package/src/types.ts +194 -0
  146. package/src/ui.ts +5 -0
  147. package/src/version.ts +3 -0
  148. package/src/workers-ai-chat.ts +333 -0
@@ -0,0 +1,620 @@
1
+ // Operator dashboard — server-rendered system health, memory, goals, cost tracking
2
+ // Auth-gated via existing bearerAuth middleware
3
+
4
+ import { getAllProceduresWithDerivedStats, getActiveAgendaItems, getActiveGoals } from './kernel/memory/index.js';
5
+ import { getMemoryStats } from './kernel/memory-adapter.js';
6
+ import type { MemoryServiceBinding } from './types.js';
7
+ import type { ProceduralEntry } from './kernel/types.js';
8
+ import { VERSION } from './version.js';
9
+
10
+ // ─── Data Model ──────────────────────────────────────────────
11
+
12
+ interface ExecutorStat {
13
+ executor: string;
14
+ count: number;
15
+ totalCost: number;
16
+ avgLatencyMs: number;
17
+ }
18
+
19
+ interface DashboardData {
20
+ version: string;
21
+ timestamp: string;
22
+ procedures: {
23
+ learned: number;
24
+ learning: number;
25
+ degraded: number;
26
+ broken: number;
27
+ total: number;
28
+ top: Array<{
29
+ pattern: string;
30
+ executor: string;
31
+ status: string;
32
+ successRate: number;
33
+ avgCost: number;
34
+ avgLatencyMs: number;
35
+ }>;
36
+ };
37
+ executorStats: ExecutorStat[];
38
+ memory: {
39
+ totalActive: number;
40
+ topics: Array<{ topic: string; count: number }>;
41
+ strengthDist: { low: number; medium: number; high: number };
42
+ recentRecalls: number;
43
+ };
44
+ goals: Array<{
45
+ id: string;
46
+ title: string;
47
+ status: string;
48
+ authority: string;
49
+ runCount: number;
50
+ lastRun: string | null;
51
+ nextRun: string | null;
52
+ }>;
53
+ agenda: Array<{
54
+ id: number;
55
+ item: string;
56
+ priority: string;
57
+ ageDays: number;
58
+ context: string | null;
59
+ }>;
60
+ cost: {
61
+ total24h: number;
62
+ total7d: number;
63
+ };
64
+ recentEpisodes: Array<{
65
+ summary: string;
66
+ executor: string;
67
+ outcome: string;
68
+ cost: number;
69
+ latencyMs: number;
70
+ createdAt: string;
71
+ }>;
72
+ lastHeartbeat: {
73
+ severity: string;
74
+ summary: string;
75
+ createdAt: string;
76
+ } | null;
77
+ tasks: {
78
+ pending: number;
79
+ proposed: number;
80
+ running: number;
81
+ completed24h: number;
82
+ failed24h: number;
83
+ recent: Array<{
84
+ id: string;
85
+ title: string;
86
+ repo: string;
87
+ status: string;
88
+ authority: string;
89
+ category: string;
90
+ exit_code: number | null;
91
+ pr_url: string | null;
92
+ completed_at: string | null;
93
+ }>;
94
+ };
95
+ }
96
+
97
+ // ─── Data Fetching ───────────────────────────────────────────
98
+
99
+ function parseExecutorFromSummary(summary: string): string {
100
+ const match = summary.match(/ via ([^:]+):/);
101
+ return match ? match[1].trim() : 'unknown';
102
+ }
103
+
104
+ export async function getDashboardData(db: D1Database, memoryBinding?: MemoryServiceBinding): Promise<DashboardData> {
105
+ const [
106
+ procedures,
107
+ agendaItems,
108
+ goals,
109
+ memStats,
110
+ cost24h,
111
+ cost7d,
112
+ episodeRows,
113
+ heartbeatRow,
114
+ taskStatusRows,
115
+ taskCompletedRows,
116
+ taskRecentRows,
117
+ ] = await Promise.all([
118
+ getAllProceduresWithDerivedStats(db, { reader: 'dashboard' }),
119
+ getActiveAgendaItems(db),
120
+ getActiveGoals(db),
121
+ memoryBinding ? getMemoryStats(memoryBinding) : Promise.resolve({ total_active: 0, topics: [], recalled_last_24h: 0, strength_distribution: { low: 0, medium: 0, high: 0 } }),
122
+ db.prepare("SELECT COALESCE(SUM(cost), 0) as total FROM episodic_memory WHERE created_at > datetime('now', '-1 day')").first<{ total: number }>(),
123
+ db.prepare("SELECT COALESCE(SUM(cost), 0) as total FROM episodic_memory WHERE created_at > datetime('now', '-7 days')").first<{ total: number }>(),
124
+ db.prepare("SELECT summary, outcome, cost, latency_ms, created_at FROM episodic_memory ORDER BY created_at DESC LIMIT 20").all<{ summary: string; outcome: string; cost: number; latency_ms: number; created_at: string }>(),
125
+ db.prepare("SELECT severity, summary, created_at FROM heartbeat_results ORDER BY created_at DESC LIMIT 1").first<{ severity: string; summary: string; created_at: string }>(),
126
+ db.prepare("SELECT status, authority, COUNT(*) as c FROM cc_tasks WHERE status IN ('pending', 'running') GROUP BY status, authority").all<{ status: string; authority: string; c: number }>(),
127
+ db.prepare("SELECT status, COUNT(*) as c FROM cc_tasks WHERE completed_at > datetime('now', '-24 hours') GROUP BY status").all<{ status: string; c: number }>(),
128
+ db.prepare("SELECT id, title, repo, status, authority, category, exit_code, pr_url, completed_at FROM cc_tasks ORDER BY COALESCE(completed_at, started_at, created_at) DESC LIMIT 10").all<{ id: string; title: string; repo: string; status: string; authority: string; category: string; exit_code: number | null; pr_url: string | null; completed_at: string | null }>(),
129
+ ]);
130
+
131
+ // Build executor stats from episodes
132
+ const execMap = new Map<string, { count: number; totalCost: number; totalLatency: number }>();
133
+ for (const ep of episodeRows.results) {
134
+ const executor = parseExecutorFromSummary(ep.summary);
135
+ const stat = execMap.get(executor) ?? { count: 0, totalCost: 0, totalLatency: 0 };
136
+ stat.count++;
137
+ stat.totalCost += ep.cost;
138
+ stat.totalLatency += ep.latency_ms;
139
+ execMap.set(executor, stat);
140
+ }
141
+ const executorStats: ExecutorStat[] = [...execMap.entries()]
142
+ .map(([executor, s]) => ({ executor, count: s.count, totalCost: s.totalCost, avgLatencyMs: Math.round(s.totalLatency / s.count) }))
143
+ .sort((a, b) => b.count - a.count);
144
+
145
+ // Procedure summary
146
+ const procTop = [...procedures]
147
+ .sort((a, b) => (b.success_count + b.fail_count) - (a.success_count + a.fail_count))
148
+ .slice(0, 8)
149
+ .map(p => {
150
+ const total = p.success_count + p.fail_count;
151
+ return {
152
+ pattern: p.task_pattern,
153
+ executor: p.executor,
154
+ status: p.status,
155
+ successRate: total > 0 ? p.success_count / total : 0,
156
+ avgCost: p.avg_cost,
157
+ avgLatencyMs: p.avg_latency_ms,
158
+ };
159
+ });
160
+
161
+ const now = Date.now();
162
+
163
+ return {
164
+ version: VERSION,
165
+ timestamp: new Date().toISOString(),
166
+ procedures: {
167
+ learned: procedures.filter(p => p.status === 'learned').length,
168
+ learning: procedures.filter(p => p.status === 'learning').length,
169
+ degraded: procedures.filter(p => p.status === 'degraded').length,
170
+ broken: procedures.filter(p => p.status === 'broken').length,
171
+ total: procedures.length,
172
+ top: procTop,
173
+ },
174
+ executorStats,
175
+ memory: {
176
+ totalActive: memStats.total_active,
177
+ topics: memStats.topics,
178
+ strengthDist: memStats.strength_distribution,
179
+ recentRecalls: memStats.recalled_last_24h,
180
+ },
181
+ goals: goals.map(g => ({
182
+ id: g.id,
183
+ title: g.title,
184
+ status: g.status,
185
+ authority: g.authority_level,
186
+ runCount: g.run_count,
187
+ lastRun: g.last_run_at,
188
+ nextRun: g.next_run_at,
189
+ })),
190
+ agenda: agendaItems.map(a => {
191
+ const ts = a.created_at.endsWith('Z') ? a.created_at : a.created_at + 'Z';
192
+ return {
193
+ id: a.id,
194
+ item: a.item,
195
+ priority: a.priority,
196
+ ageDays: Math.max(0, (now - new Date(ts).getTime()) / 86_400_000),
197
+ context: a.context,
198
+ };
199
+ }),
200
+ cost: {
201
+ total24h: cost24h?.total ?? 0,
202
+ total7d: cost7d?.total ?? 0,
203
+ },
204
+ recentEpisodes: episodeRows.results.map(e => ({
205
+ summary: e.summary,
206
+ executor: parseExecutorFromSummary(e.summary),
207
+ outcome: e.outcome,
208
+ cost: e.cost,
209
+ latencyMs: e.latency_ms,
210
+ createdAt: e.created_at,
211
+ })),
212
+ lastHeartbeat: heartbeatRow ? {
213
+ severity: heartbeatRow.severity,
214
+ summary: heartbeatRow.summary,
215
+ createdAt: heartbeatRow.created_at,
216
+ } : null,
217
+ tasks: {
218
+ pending: taskStatusRows.results.filter(r => r.status === 'pending' && r.authority !== 'proposed').reduce((s, r) => s + r.c, 0),
219
+ proposed: taskStatusRows.results.filter(r => r.authority === 'proposed').reduce((s, r) => s + r.c, 0),
220
+ running: taskStatusRows.results.filter(r => r.status === 'running').reduce((s, r) => s + r.c, 0),
221
+ completed24h: taskCompletedRows.results.find(r => r.status === 'completed')?.c ?? 0,
222
+ failed24h: taskCompletedRows.results.find(r => r.status === 'failed')?.c ?? 0,
223
+ recent: taskRecentRows.results,
224
+ },
225
+ };
226
+ }
227
+
228
+ // ─── HTML Renderer ───────────────────────────────────────────
229
+
230
+ function esc(s: string): string {
231
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
232
+ }
233
+
234
+ function priorityColor(p: string): string {
235
+ if (p === 'high') return '#ff6b6b';
236
+ if (p === 'medium') return '#ffd93d';
237
+ return '#555';
238
+ }
239
+
240
+ function executorColor(e: string): string {
241
+ const map: Record<string, string> = {
242
+ groq: '#3dd6c8',
243
+ workers_ai: '#2dd4a0',
244
+ gpt_oss: '#f59e0b',
245
+ composite: '#8b8bff',
246
+ claude: '#6366f1',
247
+ claude_opus: '#4338ca',
248
+ };
249
+ return map[e] ?? '#666';
250
+ }
251
+
252
+ function statusBadge(status: string): string {
253
+ const colors: Record<string, string> = {
254
+ learned: '#2dd4a0',
255
+ learning: '#ffd93d',
256
+ degraded: '#ff6b6b',
257
+ broken: '#ff4444',
258
+ active: '#2dd4a0',
259
+ paused: '#ffd93d',
260
+ completed: '#666',
261
+ failed: '#ff6b6b',
262
+ };
263
+ const color = colors[status] ?? '#666';
264
+ return `<span style="display:inline-block;background:${color}20;color:${color};font-size:10px;font-weight:600;padding:2px 6px;border-radius:3px;text-transform:uppercase;letter-spacing:0.5px">${esc(status)}</span>`;
265
+ }
266
+
267
+ export function dashboardPage(data: DashboardData): string {
268
+ // ── System overview metrics
269
+ const sysCards = [
270
+ { label: 'Learned', value: data.procedures.learned, color: '#2dd4a0' },
271
+ { label: 'Learning', value: data.procedures.learning, color: '#ffd93d' },
272
+ { label: 'Degraded', value: data.procedures.degraded, color: '#ff6b6b' },
273
+ { label: 'Broken', value: data.procedures.broken, color: '#ff4444' },
274
+ ];
275
+
276
+ // ── Executor bar chart (CSS only)
277
+ const maxCount = Math.max(1, ...data.executorStats.map(e => e.count));
278
+ const execBars = data.executorStats.map(e => {
279
+ const pct = (e.count / maxCount) * 100;
280
+ const color = executorColor(e.executor);
281
+ return `<div style="margin-bottom:8px">
282
+ <div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:3px">
283
+ <span style="font-family:'JetBrains Mono',monospace;color:${color}">${esc(e.executor)}</span>
284
+ <span style="color:var(--text-secondary)">${e.count}x &middot; $${e.totalCost.toFixed(4)} &middot; ${e.avgLatencyMs}ms</span>
285
+ </div>
286
+ <div style="height:6px;background:var(--bg-deep);border-radius:3px;overflow:hidden">
287
+ <div style="width:${pct}%;height:100%;background:${color};border-radius:3px;transition:width 0.3s"></div>
288
+ </div>
289
+ </div>`;
290
+ }).join('');
291
+
292
+ // ── Memory topic breakdown
293
+ const topicList = data.memory.topics.map(t =>
294
+ `<div style="display:flex;justify-content:space-between;padding:3px 0;font-size:12px;border-bottom:1px solid var(--border-subtle)">
295
+ <span style="color:var(--text-primary)">${esc(t.topic)}</span>
296
+ <span style="color:var(--text-secondary);font-family:'JetBrains Mono',monospace">${t.count}</span>
297
+ </div>`
298
+ ).join('');
299
+
300
+ // ── Strength distribution bar
301
+ const stTotal = data.memory.strengthDist.low + data.memory.strengthDist.medium + data.memory.strengthDist.high;
302
+ const stBar = stTotal > 0 ? `<div style="display:flex;height:8px;border-radius:4px;overflow:hidden;margin-top:8px">
303
+ <div style="width:${(data.memory.strengthDist.low / stTotal) * 100}%;background:#ff6b6b" title="Low (1)"></div>
304
+ <div style="width:${(data.memory.strengthDist.medium / stTotal) * 100}%;background:#ffd93d" title="Medium (2-4)"></div>
305
+ <div style="width:${(data.memory.strengthDist.high / stTotal) * 100}%;background:#2dd4a0" title="High (5+)"></div>
306
+ </div>
307
+ <div style="display:flex;justify-content:space-between;margin-top:4px;font-size:10px;color:var(--text-secondary)">
308
+ <span>Low: ${data.memory.strengthDist.low}</span>
309
+ <span>Med: ${data.memory.strengthDist.medium}</span>
310
+ <span>High: ${data.memory.strengthDist.high}</span>
311
+ </div>` : '';
312
+
313
+ // ── Goals list
314
+ const goalsList = data.goals.map(g => {
315
+ const lastRun = g.lastRun ? new Date(g.lastRun + (g.lastRun.endsWith('Z') ? '' : 'Z')).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : 'never';
316
+ return `<div style="background:var(--bg-deep);border:1px solid var(--border-subtle);border-radius:6px;padding:10px 12px;margin-bottom:6px">
317
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
318
+ <span style="font-size:13px;color:var(--text-primary);font-weight:500">${esc(g.title)}</span>
319
+ ${statusBadge(g.status)}
320
+ </div>
321
+ <div style="font-size:11px;color:var(--text-secondary);font-family:'JetBrains Mono',monospace">
322
+ ${g.authority} &middot; ${g.runCount} runs &middot; last: ${lastRun}
323
+ </div>
324
+ </div>`;
325
+ }).join('');
326
+
327
+ // ── Agenda items
328
+ const agendaList = data.agenda.map(a => {
329
+ const age = a.ageDays < 1 ? `${Math.round(a.ageDays * 24)}h` : `${Math.floor(a.ageDays)}d`;
330
+ const isEscalated = a.context?.includes('[escalated:') ?? false;
331
+ return `<div style="background:var(--bg-deep);border:1px solid var(--border-subtle);border-left:3px solid ${priorityColor(a.priority)};border-radius:6px;padding:8px 12px;margin-bottom:6px">
332
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:2px">
333
+ <span style="font-size:10px;color:var(--text-secondary);font-family:'JetBrains Mono',monospace">#${a.id} &middot; ${esc(a.priority)} &middot; ${age} ago${isEscalated ? ' &middot; <span style="color:#ff6b6b">ESCALATED</span>' : ''}</span>
334
+ </div>
335
+ <span style="font-size:12px;color:var(--text-primary)">${esc(a.item.slice(0, 120))}</span>
336
+ </div>`;
337
+ }).join('');
338
+
339
+ // ── Recent episodes
340
+ const episodeList = data.recentEpisodes.slice(0, 10).map(e => {
341
+ const time = e.createdAt.slice(11, 16);
342
+ const color = executorColor(e.executor);
343
+ const outcomeColor = e.outcome === 'success' ? '#2dd4a0' : '#ff6b6b';
344
+ return `<div style="display:flex;gap:8px;padding:4px 0;font-size:11px;border-bottom:1px solid var(--border-subtle);align-items:center">
345
+ <span style="color:var(--text-secondary);font-family:'JetBrains Mono',monospace;min-width:40px">${time}</span>
346
+ <span style="color:${color};font-family:'JetBrains Mono',monospace;min-width:70px">${esc(e.executor)}</span>
347
+ <span style="color:${outcomeColor};min-width:14px">${e.outcome === 'success' ? '+' : '-'}</span>
348
+ <span style="color:var(--text-secondary);font-family:'JetBrains Mono',monospace;min-width:55px">$${e.cost.toFixed(4)}</span>
349
+ <span style="color:var(--text-primary);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(e.summary.slice(0, 80))}</span>
350
+ </div>`;
351
+ }).join('');
352
+
353
+ // ── Heartbeat status
354
+ const hb = data.lastHeartbeat;
355
+ const hbColor = !hb ? '#666' : hb.severity === 'critical' ? '#ff4444' : hb.severity === 'high' ? '#ff6b6b' : hb.severity === 'medium' ? '#ffd93d' : '#2dd4a0';
356
+ const hbLabel = hb ? `${hb.severity.toUpperCase()} — ${hb.createdAt.slice(0, 16)}` : 'No data';
357
+
358
+ return `<!DOCTYPE html>
359
+ <html lang="en">
360
+ <head>
361
+ <meta charset="utf-8">
362
+ <meta name="viewport" content="width=device-width, initial-scale=1">
363
+ <meta http-equiv="refresh" content="300">
364
+ <title>AEGIS Dashboard</title>
365
+ <link rel="preconnect" href="https://fonts.googleapis.com">
366
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
367
+ <link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;700;800&family=JetBrains+Mono:wght@300;400;500&family=Instrument+Sans:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet">
368
+ <style>
369
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
370
+ :root {
371
+ --bg-deep: #04040a;
372
+ --bg-surface: #080812;
373
+ --bg-card: #0b0b18;
374
+ --border-subtle: rgba(123, 123, 223, 0.06);
375
+ --border-medium: rgba(123, 123, 223, 0.1);
376
+ --accent: #7b7bdf;
377
+ --accent-glow: #8b8bff;
378
+ --accent-teal: #3dd6c8;
379
+ --text-primary: #c8c8d8;
380
+ --text-secondary: #6a6a80;
381
+ --text-dim: #2e2e3e;
382
+ }
383
+ html { -webkit-font-smoothing: antialiased; }
384
+ body {
385
+ background: var(--bg-deep);
386
+ color: var(--text-primary);
387
+ font-family: 'Instrument Sans', sans-serif;
388
+ padding: 20px;
389
+ min-height: 100vh;
390
+ }
391
+ .header {
392
+ display: flex;
393
+ justify-content: space-between;
394
+ align-items: center;
395
+ margin-bottom: 20px;
396
+ padding-bottom: 16px;
397
+ border-bottom: 1px solid var(--border-medium);
398
+ }
399
+ .header h1 {
400
+ font-family: 'Syne', sans-serif;
401
+ font-size: 22px;
402
+ font-weight: 700;
403
+ color: var(--accent-glow);
404
+ letter-spacing: -0.5px;
405
+ }
406
+ .header .meta {
407
+ font-size: 11px;
408
+ color: var(--text-secondary);
409
+ font-family: 'JetBrains Mono', monospace;
410
+ text-align: right;
411
+ }
412
+ .grid {
413
+ display: grid;
414
+ grid-template-columns: 1fr 1fr;
415
+ gap: 16px;
416
+ }
417
+ .card {
418
+ background: var(--bg-card);
419
+ border: 1px solid var(--border-subtle);
420
+ border-radius: 8px;
421
+ padding: 16px;
422
+ }
423
+ .card.full { grid-column: 1 / -1; }
424
+ .card h2 {
425
+ font-family: 'Syne', sans-serif;
426
+ font-size: 13px;
427
+ font-weight: 700;
428
+ color: var(--text-secondary);
429
+ text-transform: uppercase;
430
+ letter-spacing: 1px;
431
+ margin-bottom: 12px;
432
+ }
433
+ .stat-row {
434
+ display: flex;
435
+ gap: 12px;
436
+ flex-wrap: wrap;
437
+ }
438
+ .stat-box {
439
+ flex: 1;
440
+ min-width: 80px;
441
+ background: var(--bg-deep);
442
+ border: 1px solid var(--border-subtle);
443
+ border-radius: 6px;
444
+ padding: 10px 12px;
445
+ text-align: center;
446
+ }
447
+ .stat-box .value {
448
+ font-family: 'JetBrains Mono', monospace;
449
+ font-size: 24px;
450
+ font-weight: 500;
451
+ line-height: 1;
452
+ }
453
+ .stat-box .label {
454
+ font-size: 10px;
455
+ color: var(--text-secondary);
456
+ text-transform: uppercase;
457
+ letter-spacing: 0.5px;
458
+ margin-top: 4px;
459
+ }
460
+ .cost-row {
461
+ display: flex;
462
+ gap: 16px;
463
+ margin-bottom: 12px;
464
+ }
465
+ .cost-item {
466
+ background: var(--bg-deep);
467
+ border: 1px solid var(--border-subtle);
468
+ border-radius: 6px;
469
+ padding: 10px 16px;
470
+ flex: 1;
471
+ }
472
+ .cost-item .amount {
473
+ font-family: 'JetBrains Mono', monospace;
474
+ font-size: 20px;
475
+ color: var(--accent-teal);
476
+ }
477
+ .cost-item .period {
478
+ font-size: 11px;
479
+ color: var(--text-secondary);
480
+ margin-top: 2px;
481
+ }
482
+ .nav-links {
483
+ display: flex;
484
+ gap: 12px;
485
+ }
486
+ .nav-links a {
487
+ color: var(--accent);
488
+ text-decoration: none;
489
+ font-size: 12px;
490
+ font-family: 'JetBrains Mono', monospace;
491
+ }
492
+ .nav-links a:hover { color: var(--accent-glow); }
493
+ @media (max-width: 768px) {
494
+ .grid { grid-template-columns: 1fr; }
495
+ .card.full { grid-column: 1; }
496
+ body { padding: 12px; }
497
+ }
498
+ </style>
499
+ </head>
500
+ <body>
501
+ <div class="header">
502
+ <div>
503
+ <h1>AEGIS Dashboard</h1>
504
+ <div class="nav-links">
505
+ <a href="/chat">Chat</a>
506
+ <a href="/health">Health</a>
507
+ <a href="/">Landing</a>
508
+ <a href="https://stackbilt.dev">EdgeStack</a>
509
+ <a href="https://docs.stackbilt.dev">Docs</a>
510
+ </div>
511
+ </div>
512
+ <div class="meta">
513
+ v${esc(data.version)}<br>
514
+ <span style="color:${hbColor}">${esc(hbLabel)}</span><br>
515
+ ${esc(data.timestamp.slice(0, 19))}
516
+ </div>
517
+ </div>
518
+
519
+ <div class="grid">
520
+ <!-- System Overview -->
521
+ <div class="card full">
522
+ <h2>Kernel Procedures</h2>
523
+ <div class="stat-row">
524
+ ${sysCards.map(c => `<div class="stat-box">
525
+ <div class="value" style="color:${c.color}">${c.value}</div>
526
+ <div class="label">${c.label}</div>
527
+ </div>`).join('')}
528
+ <div class="stat-box">
529
+ <div class="value" style="color:var(--text-primary)">${data.procedures.total}</div>
530
+ <div class="label">Total</div>
531
+ </div>
532
+ </div>
533
+ </div>
534
+
535
+ <!-- Executor Routing -->
536
+ <div class="card">
537
+ <h2>Executor Routing (Last 20)</h2>
538
+ ${execBars || '<p style="font-size:12px;color:var(--text-secondary)">No episodes yet</p>'}
539
+ </div>
540
+
541
+ <!-- Memory Health -->
542
+ <div class="card">
543
+ <h2>Memory Health</h2>
544
+ <div class="stat-row" style="margin-bottom:12px">
545
+ <div class="stat-box">
546
+ <div class="value" style="color:var(--accent-teal)">${data.memory.totalActive}</div>
547
+ <div class="label">Active</div>
548
+ </div>
549
+ <div class="stat-box">
550
+ <div class="value" style="color:var(--accent)">${data.memory.recentRecalls}</div>
551
+ <div class="label">Recalled 24h</div>
552
+ </div>
553
+ </div>
554
+ <div style="font-size:11px;color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px">Strength Distribution</div>
555
+ ${stBar}
556
+ <div style="font-size:11px;color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.5px;margin:12px 0 6px">Topics</div>
557
+ ${topicList || '<p style="font-size:12px;color:var(--text-secondary)">No memory entries</p>'}
558
+ </div>
559
+
560
+ <!-- Goals -->
561
+ <div class="card">
562
+ <h2>Active Goals (${data.goals.length})</h2>
563
+ ${goalsList || '<p style="font-size:12px;color:var(--text-secondary)">No active goals</p>'}
564
+ </div>
565
+
566
+ <!-- Agenda -->
567
+ <div class="card">
568
+ <h2>Agenda (${data.agenda.length})</h2>
569
+ ${agendaList || '<p style="font-size:12px;color:var(--text-secondary)">No active items</p>'}
570
+ </div>
571
+
572
+ <!-- Task Queue -->
573
+ <div class="card full">
574
+ <h2>Task Queue</h2>
575
+ <div class="stat-row" style="margin-bottom:12px">
576
+ <div class="stat-box"><div class="value" style="color:#ffd93d">${data.tasks.pending}</div><div class="label">Pending</div></div>
577
+ <div class="stat-box"><div class="value" style="color:#a855f7">${data.tasks.proposed}</div><div class="label">Proposed</div></div>
578
+ <div class="stat-box"><div class="value" style="color:#3dd6c8">${data.tasks.running}</div><div class="label">Running</div></div>
579
+ <div class="stat-box"><div class="value" style="color:#2dd4a0">${data.tasks.completed24h}</div><div class="label">Done 24h</div></div>
580
+ <div class="stat-box"><div class="value" style="color:#ff6b6b">${data.tasks.failed24h}</div><div class="label">Failed 24h</div></div>
581
+ </div>
582
+ ${data.tasks.recent.map(t => {
583
+ const statusColors: Record<string, string> = { completed: '#2dd4a0', failed: '#ff6b6b', running: '#3dd6c8', pending: '#ffd93d', cancelled: '#666' };
584
+ const exitLabel = t.exit_code !== null ? ` · exit ${t.exit_code}` : '';
585
+ return `<div style="display:flex;gap:8px;padding:4px 0;font-size:11px;border-bottom:1px solid var(--border-subtle);align-items:center">
586
+ ${statusBadge(t.status)}
587
+ <span style="color:var(--text-secondary);font-family:'JetBrains Mono',monospace;min-width:50px">${esc(t.repo)}</span>
588
+ <span style="display:inline-block;background:${statusColors[t.status] ?? '#666'}15;color:var(--text-secondary);font-size:9px;padding:1px 4px;border-radius:2px">${esc(t.category)}</span>
589
+ ${t.authority === 'proposed' ? '<span style="color:#a855f7;font-size:9px">[PROPOSED]</span>' : ''}
590
+ <span style="color:var(--text-primary);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(t.title)}</span>
591
+ ${t.pr_url ? `<a href="${esc(t.pr_url)}" target="_blank" style="color:#3dd6c8;font-size:9px;text-decoration:none" title="View PR">PR</a>` : ''}
592
+ <span style="color:var(--text-secondary);font-family:'JetBrains Mono',monospace;font-size:10px">${exitLabel}</span>
593
+ </div>`;
594
+ }).join('') || '<p style="font-size:12px;color:var(--text-secondary)">No tasks</p>'}
595
+ </div>
596
+
597
+ <!-- Cost Tracking -->
598
+ <div class="card full">
599
+ <h2>Cost Tracking</h2>
600
+ <div class="cost-row">
601
+ <div class="cost-item">
602
+ <div class="amount">$${data.cost.total24h.toFixed(4)}</div>
603
+ <div class="period">Last 24 hours</div>
604
+ </div>
605
+ <div class="cost-item">
606
+ <div class="amount">$${data.cost.total7d.toFixed(4)}</div>
607
+ <div class="period">Last 7 days</div>
608
+ </div>
609
+ </div>
610
+ </div>
611
+
612
+ <!-- Recent Episodes -->
613
+ <div class="card full">
614
+ <h2>Recent Dispatches</h2>
615
+ ${episodeList || '<p style="font-size:12px;color:var(--text-secondary)">No episodes yet</p>'}
616
+ </div>
617
+ </div>
618
+ </body>
619
+ </html>`;
620
+ }