@agent-e/server 1.5.13 → 1.6.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.
@@ -0,0 +1,1574 @@
1
+ // src/AgentEServer.ts
2
+ import * as http from "http";
3
+ import {
4
+ AgentE,
5
+ Observer,
6
+ Diagnoser,
7
+ ALL_PRINCIPLES,
8
+ DEFAULT_THRESHOLDS
9
+ } from "@agent-e/core";
10
+
11
+ // src/routes.ts
12
+ import { validateEconomyState } from "@agent-e/core";
13
+
14
+ // src/dashboard.ts
15
+ function getDashboardHtml() {
16
+ return `<!DOCTYPE html>
17
+ <html lang="en">
18
+ <head>
19
+ <meta charset="UTF-8">
20
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
21
+ <title>AgentE Dashboard</title>
22
+ <link rel="preconnect" href="https://fonts.googleapis.com">
23
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
24
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
25
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script>
26
+ <style>
27
+ :root {
28
+ --bg-root: #09090b;
29
+ --bg-panel: #18181b;
30
+ --bg-panel-hover: #1f1f23;
31
+ --border: #27272a;
32
+ --border-light: #3f3f46;
33
+ --text-primary: #f4f4f5;
34
+ --text-secondary: #a1a1aa;
35
+ --text-muted: #71717a;
36
+ --text-dim: #52525b;
37
+ --accent: #22c55e;
38
+ --accent-dim: #166534;
39
+ --warning: #eab308;
40
+ --warning-dim: #854d0e;
41
+ --danger: #ef4444;
42
+ --danger-dim: #991b1b;
43
+ --blue: #3b82f6;
44
+ --font-sans: 'IBM Plex Sans', system-ui, sans-serif;
45
+ --font-mono: 'JetBrains Mono', monospace;
46
+ }
47
+
48
+ * { margin: 0; padding: 0; box-sizing: border-box; }
49
+
50
+ body {
51
+ background: var(--bg-root);
52
+ color: var(--text-primary);
53
+ font-family: var(--font-sans);
54
+ font-size: 14px;
55
+ line-height: 1.5;
56
+ overflow-x: hidden;
57
+ }
58
+
59
+ /* \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
60
+ .header {
61
+ position: sticky;
62
+ top: 0;
63
+ z-index: 100;
64
+ background: var(--bg-root);
65
+ border-bottom: 1px solid var(--border);
66
+ padding: 12px 24px;
67
+ display: flex;
68
+ align-items: center;
69
+ gap: 24px;
70
+ backdrop-filter: blur(8px);
71
+ }
72
+
73
+ .header-brand {
74
+ font-weight: 600;
75
+ font-size: 16px;
76
+ color: var(--text-primary);
77
+ white-space: nowrap;
78
+ }
79
+
80
+ .header-brand span { color: var(--accent); }
81
+
82
+ .kpi-row {
83
+ display: flex;
84
+ gap: 20px;
85
+ flex-wrap: wrap;
86
+ align-items: center;
87
+ margin-left: auto;
88
+ }
89
+
90
+ .kpi {
91
+ display: flex;
92
+ align-items: center;
93
+ gap: 6px;
94
+ font-size: 13px;
95
+ color: var(--text-secondary);
96
+ }
97
+
98
+ .kpi-value {
99
+ font-family: var(--font-mono);
100
+ font-weight: 500;
101
+ color: var(--text-primary);
102
+ font-size: 13px;
103
+ }
104
+
105
+ .kpi-value.health-good { color: var(--accent); }
106
+ .kpi-value.health-warn { color: var(--warning); }
107
+ .kpi-value.health-bad { color: var(--danger); }
108
+
109
+ .live-dot {
110
+ width: 8px;
111
+ height: 8px;
112
+ border-radius: 50%;
113
+ background: var(--accent);
114
+ animation: pulse 2s ease-in-out infinite;
115
+ }
116
+
117
+ .live-dot.disconnected {
118
+ background: var(--danger);
119
+ animation: none;
120
+ }
121
+
122
+ @keyframes pulse {
123
+ 0%, 100% { opacity: 1; }
124
+ 50% { opacity: 0.4; }
125
+ }
126
+
127
+ /* \u2500\u2500 Layout \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
128
+ .container {
129
+ max-width: 1440px;
130
+ margin: 0 auto;
131
+ padding: 20px 24px;
132
+ display: flex;
133
+ flex-direction: column;
134
+ gap: 16px;
135
+ }
136
+
137
+ .panel {
138
+ background: var(--bg-panel);
139
+ border: 1px solid var(--border);
140
+ border-radius: 8px;
141
+ padding: 16px;
142
+ }
143
+
144
+ .panel-title {
145
+ font-size: 13px;
146
+ font-weight: 600;
147
+ color: var(--text-secondary);
148
+ text-transform: uppercase;
149
+ letter-spacing: 0.05em;
150
+ margin-bottom: 12px;
151
+ }
152
+
153
+ /* \u2500\u2500 Charts grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
154
+ .charts-grid {
155
+ display: grid;
156
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
157
+ gap: 16px;
158
+ }
159
+
160
+ .chart-box {
161
+ background: var(--bg-panel);
162
+ border: 1px solid var(--border);
163
+ border-radius: 8px;
164
+ padding: 16px;
165
+ }
166
+
167
+ .chart-box canvas { width: 100% !important; height: 160px !important; }
168
+
169
+ .chart-label {
170
+ font-size: 12px;
171
+ color: var(--text-muted);
172
+ font-weight: 500;
173
+ margin-bottom: 8px;
174
+ }
175
+
176
+ .chart-value {
177
+ font-family: var(--font-mono);
178
+ font-size: 22px;
179
+ font-weight: 500;
180
+ color: var(--text-primary);
181
+ margin-bottom: 8px;
182
+ }
183
+
184
+ /* \u2500\u2500 Terminal (Decision Feed) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
185
+ .terminal {
186
+ background: var(--bg-root);
187
+ border: 1px solid var(--border);
188
+ border-radius: 8px;
189
+ height: 380px;
190
+ overflow-y: auto;
191
+ font-family: var(--font-mono);
192
+ font-size: 12px;
193
+ line-height: 1.7;
194
+ padding: 12px 16px;
195
+ }
196
+
197
+ .terminal::-webkit-scrollbar { width: 6px; }
198
+ .terminal::-webkit-scrollbar-track { background: transparent; }
199
+ .terminal::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 3px; }
200
+
201
+ .term-line {
202
+ white-space: nowrap;
203
+ opacity: 0;
204
+ transform: translateY(4px);
205
+ animation: termIn 0.3s ease-out forwards;
206
+ }
207
+
208
+ @keyframes termIn {
209
+ to { opacity: 1; transform: translateY(0); }
210
+ }
211
+
212
+ .t-tick { color: var(--text-dim); }
213
+ .t-ok { color: var(--accent); }
214
+ .t-skip { color: var(--warning); }
215
+ .t-fail { color: var(--danger); }
216
+ .t-principle { color: var(--text-primary); font-weight: 500; }
217
+ .t-param { color: var(--text-secondary); }
218
+ .t-old { color: #d4d4d8; font-variant-numeric: tabular-nums; }
219
+ .t-arrow { color: var(--text-dim); }
220
+ .t-new { color: var(--accent); font-variant-numeric: tabular-nums; }
221
+ .t-meta { color: var(--text-dim); }
222
+
223
+ /* \u2500\u2500 Alerts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
224
+ .alerts-container {
225
+ display: flex;
226
+ flex-direction: column;
227
+ gap: 8px;
228
+ max-height: 320px;
229
+ overflow-y: auto;
230
+ }
231
+
232
+ .alert-card {
233
+ display: flex;
234
+ align-items: flex-start;
235
+ gap: 12px;
236
+ padding: 12px;
237
+ border-radius: 6px;
238
+ border: 1px solid var(--border);
239
+ background: var(--bg-panel);
240
+ transition: opacity 0.3s, transform 0.3s;
241
+ }
242
+
243
+ .alert-card.fade-out {
244
+ opacity: 0;
245
+ transform: translateX(20px);
246
+ }
247
+
248
+ .alert-severity {
249
+ font-family: var(--font-mono);
250
+ font-weight: 600;
251
+ font-size: 13px;
252
+ padding: 2px 8px;
253
+ border-radius: 4px;
254
+ white-space: nowrap;
255
+ }
256
+
257
+ .sev-high { background: var(--danger-dim); color: var(--danger); }
258
+ .sev-med { background: var(--warning-dim); color: var(--warning); }
259
+ .sev-low { background: var(--accent-dim); color: var(--accent); }
260
+
261
+ .alert-body { flex: 1; }
262
+ .alert-principle { font-weight: 500; font-size: 13px; }
263
+ .alert-reason { color: var(--text-secondary); font-size: 12px; margin-top: 2px; }
264
+
265
+ /* \u2500\u2500 Violations table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
266
+ .violations-table {
267
+ width: 100%;
268
+ border-collapse: collapse;
269
+ font-size: 12px;
270
+ }
271
+
272
+ .violations-table th {
273
+ text-align: left;
274
+ color: var(--text-muted);
275
+ font-weight: 500;
276
+ padding: 6px 10px;
277
+ border-bottom: 1px solid var(--border);
278
+ cursor: pointer;
279
+ user-select: none;
280
+ }
281
+
282
+ .violations-table th:hover { color: var(--text-secondary); }
283
+
284
+ .violations-table td {
285
+ padding: 6px 10px;
286
+ border-bottom: 1px solid var(--border);
287
+ color: var(--text-secondary);
288
+ font-family: var(--font-mono);
289
+ font-size: 11px;
290
+ }
291
+
292
+ .violations-table tr:hover td { background: var(--bg-panel-hover); }
293
+
294
+ /* \u2500\u2500 Split row \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
295
+ .split-row {
296
+ display: grid;
297
+ grid-template-columns: 1fr 1fr;
298
+ gap: 16px;
299
+ }
300
+
301
+ @media (max-width: 800px) {
302
+ .split-row { grid-template-columns: 1fr; }
303
+ }
304
+
305
+ /* \u2500\u2500 Persona bar chart \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
306
+ .persona-bars { display: flex; flex-direction: column; gap: 6px; }
307
+
308
+ .persona-row {
309
+ display: flex;
310
+ align-items: center;
311
+ gap: 8px;
312
+ font-size: 12px;
313
+ }
314
+
315
+ .persona-label {
316
+ width: 100px;
317
+ text-align: right;
318
+ color: var(--text-secondary);
319
+ font-size: 11px;
320
+ flex-shrink: 0;
321
+ }
322
+
323
+ .persona-bar-track {
324
+ flex: 1;
325
+ height: 16px;
326
+ background: var(--bg-root);
327
+ border-radius: 3px;
328
+ overflow: hidden;
329
+ }
330
+
331
+ .persona-bar-fill {
332
+ height: 100%;
333
+ background: var(--accent);
334
+ border-radius: 3px;
335
+ transition: width 0.5s ease;
336
+ }
337
+
338
+ .persona-pct {
339
+ width: 40px;
340
+ font-family: var(--font-mono);
341
+ font-size: 11px;
342
+ color: var(--text-muted);
343
+ }
344
+
345
+ /* \u2500\u2500 Registry list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
346
+ .registry-list { display: flex; flex-direction: column; gap: 4px; }
347
+
348
+ .registry-item {
349
+ display: flex;
350
+ justify-content: space-between;
351
+ align-items: center;
352
+ padding: 6px 10px;
353
+ border-radius: 4px;
354
+ font-size: 12px;
355
+ }
356
+
357
+ .registry-item:nth-child(odd) { background: rgba(255,255,255,0.02); }
358
+ .registry-key { color: var(--text-secondary); font-family: var(--font-mono); }
359
+ .registry-val { color: var(--accent); font-family: var(--font-mono); font-weight: 500; }
360
+
361
+ /* \u2500\u2500 Advisor mode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
362
+ .advisor-banner {
363
+ display: none;
364
+ background: var(--warning-dim);
365
+ border: 1px solid var(--warning);
366
+ color: var(--warning);
367
+ padding: 8px 16px;
368
+ border-radius: 6px;
369
+ font-size: 13px;
370
+ font-weight: 500;
371
+ align-items: center;
372
+ gap: 8px;
373
+ }
374
+
375
+ .advisor-mode .advisor-banner { display: flex; }
376
+
377
+ .pending-pill {
378
+ background: var(--warning);
379
+ color: var(--bg-root);
380
+ font-size: 11px;
381
+ font-weight: 600;
382
+ padding: 1px 8px;
383
+ border-radius: 10px;
384
+ font-family: var(--font-mono);
385
+ }
386
+
387
+ .advisor-btn {
388
+ font-family: var(--font-mono);
389
+ font-size: 11px;
390
+ padding: 2px 10px;
391
+ border-radius: 4px;
392
+ border: none;
393
+ cursor: pointer;
394
+ font-weight: 500;
395
+ transition: opacity 0.15s;
396
+ }
397
+
398
+ .advisor-btn:hover { opacity: 0.85; }
399
+ .advisor-btn.approve { background: var(--accent); color: var(--bg-root); }
400
+ .advisor-btn.reject { background: var(--danger); color: #fff; }
401
+
402
+ .advisor-actions { display: none; gap: 6px; margin-left: 8px; }
403
+ .advisor-mode .advisor-actions { display: inline-flex; }
404
+
405
+ /* \u2500\u2500 Empty state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
406
+ .empty-state {
407
+ color: var(--text-dim);
408
+ font-size: 13px;
409
+ text-align: center;
410
+ padding: 40px 20px;
411
+ }
412
+
413
+ /* \u2500\u2500 Reduced motion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
414
+ @media (prefers-reduced-motion: reduce) {
415
+ .term-line { animation: none; opacity: 1; transform: none; }
416
+ .live-dot { animation: none; }
417
+ .persona-bar-fill { transition: none; }
418
+ }
419
+ </style>
420
+ </head>
421
+ <body>
422
+
423
+ <!-- Header -->
424
+ <div class="header" id="header">
425
+ <div class="header-brand">Agent<span>E</span> v1.6</div>
426
+ <div class="kpi-row">
427
+ <div class="kpi">Health <span class="kpi-value health-good" id="kpi-health">--</span></div>
428
+ <div class="kpi">Mode <span class="kpi-value" id="kpi-mode">--</span></div>
429
+ <div class="kpi">Tick <span class="kpi-value" id="kpi-tick">0</span></div>
430
+ <div class="kpi">Uptime <span class="kpi-value" id="kpi-uptime">0s</span></div>
431
+ <div class="kpi">Plans <span class="kpi-value" id="kpi-plans">0</span></div>
432
+ <div class="live-dot" id="live-dot" title="WebSocket connected"></div>
433
+ </div>
434
+ </div>
435
+
436
+ <div class="container" id="app">
437
+ <!-- Advisor banner -->
438
+ <div class="advisor-banner" id="advisor-banner">
439
+ ADVISOR MODE \u2014 Recommendations require manual approval
440
+ <span class="pending-pill" id="pending-count">0</span> pending
441
+ </div>
442
+
443
+ <!-- Charts -->
444
+ <div class="charts-grid">
445
+ <div class="chart-box">
446
+ <div class="chart-label">Economy Health</div>
447
+ <div class="chart-value" id="cv-health">--</div>
448
+ <canvas id="chart-health"></canvas>
449
+ </div>
450
+ <div class="chart-box">
451
+ <div class="chart-label">Gini Coefficient</div>
452
+ <div class="chart-value" id="cv-gini">--</div>
453
+ <canvas id="chart-gini"></canvas>
454
+ </div>
455
+ <div class="chart-box">
456
+ <div class="chart-label">Net Flow</div>
457
+ <div class="chart-value" id="cv-netflow">--</div>
458
+ <canvas id="chart-netflow"></canvas>
459
+ </div>
460
+ <div class="chart-box">
461
+ <div class="chart-label">Avg Satisfaction</div>
462
+ <div class="chart-value" id="cv-satisfaction">--</div>
463
+ <canvas id="chart-satisfaction"></canvas>
464
+ </div>
465
+ </div>
466
+
467
+ <!-- Decision Feed -->
468
+ <div class="panel">
469
+ <div class="panel-title">Decision Feed</div>
470
+ <div class="terminal" id="terminal"></div>
471
+ </div>
472
+
473
+ <!-- Active Alerts -->
474
+ <div class="panel">
475
+ <div class="panel-title">Active Alerts</div>
476
+ <div class="alerts-container" id="alerts-container">
477
+ <div class="empty-state" id="alerts-empty">No active violations</div>
478
+ </div>
479
+ </div>
480
+
481
+ <!-- Violation History -->
482
+ <div class="panel">
483
+ <div class="panel-title">Violation History</div>
484
+ <div style="max-height:320px;overflow-y:auto">
485
+ <table class="violations-table" id="violations-table">
486
+ <thead>
487
+ <tr>
488
+ <th data-sort="tick">Tick</th>
489
+ <th data-sort="principle">Principle</th>
490
+ <th data-sort="severity">Severity</th>
491
+ <th data-sort="parameter">Parameter</th>
492
+ <th data-sort="result">Result</th>
493
+ </tr>
494
+ </thead>
495
+ <tbody id="violations-body"></tbody>
496
+ </table>
497
+ </div>
498
+ </div>
499
+
500
+ <!-- Split: Personas + Registry -->
501
+ <div class="split-row">
502
+ <div class="panel">
503
+ <div class="panel-title">Persona Distribution</div>
504
+ <div class="persona-bars" id="persona-bars">
505
+ <div class="empty-state">No persona data yet</div>
506
+ </div>
507
+ </div>
508
+ <div class="panel">
509
+ <div class="panel-title">Parameter Registry</div>
510
+ <div class="registry-list" id="registry-list">
511
+ <div class="empty-state">No parameters registered</div>
512
+ </div>
513
+ </div>
514
+ </div>
515
+ </div>
516
+
517
+ <script>
518
+ (function() {
519
+ 'use strict';
520
+
521
+ // \u2500\u2500 State \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
522
+ let ws = null;
523
+ let reconnectDelay = 1000;
524
+ const MAX_RECONNECT = 30000;
525
+ let isAdvisor = false;
526
+ let pendingDecisions = [];
527
+ const MAX_TERMINAL_LINES = 80;
528
+ const MAX_VIOLATIONS = 100;
529
+ let violationSortKey = 'tick';
530
+ let violationSortAsc = false;
531
+ let violations = [];
532
+
533
+ // Chart instances
534
+ let chartHealth, chartGini, chartNetflow, chartSatisfaction;
535
+
536
+ // \u2500\u2500 DOM refs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
537
+ const $kpiHealth = document.getElementById('kpi-health');
538
+ const $kpiMode = document.getElementById('kpi-mode');
539
+ const $kpiTick = document.getElementById('kpi-tick');
540
+ const $kpiUptime = document.getElementById('kpi-uptime');
541
+ const $kpiPlans = document.getElementById('kpi-plans');
542
+ const $liveDot = document.getElementById('live-dot');
543
+ const $terminal = document.getElementById('terminal');
544
+ const $alertsContainer = document.getElementById('alerts-container');
545
+ const $alertsEmpty = document.getElementById('alerts-empty');
546
+ const $violationsBody = document.getElementById('violations-body');
547
+ const $personaBars = document.getElementById('persona-bars');
548
+ const $registryList = document.getElementById('registry-list');
549
+ const $pendingCount = document.getElementById('pending-count');
550
+ const $app = document.getElementById('app');
551
+
552
+ // \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
553
+ function pad(n, w) { return String(n).padStart(w || 4, ' '); }
554
+ function fmt(n) { return typeof n === 'number' ? n.toFixed(3) : '\u2014'; }
555
+ function pct(n) { return typeof n === 'number' ? (n * 100).toFixed(0) + '%' : '\u2014'; }
556
+
557
+ function formatUptime(ms) {
558
+ const s = Math.floor(ms / 1000);
559
+ if (s < 60) return s + 's';
560
+ if (s < 3600) return Math.floor(s / 60) + 'm ' + (s % 60) + 's';
561
+ const h = Math.floor(s / 3600);
562
+ return h + 'h ' + Math.floor((s % 3600) / 60) + 'm';
563
+ }
564
+
565
+ function healthClass(h) {
566
+ if (h >= 70) return 'health-good';
567
+ if (h >= 40) return 'health-warn';
568
+ return 'health-bad';
569
+ }
570
+
571
+ function sevClass(s) {
572
+ if (s >= 7) return 'sev-high';
573
+ if (s >= 4) return 'sev-med';
574
+ return 'sev-low';
575
+ }
576
+
577
+ // \u2500\u2500 Chart setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
578
+ const chartOpts = {
579
+ responsive: true,
580
+ maintainAspectRatio: false,
581
+ animation: { duration: 300 },
582
+ plugins: { legend: { display: false } },
583
+ scales: {
584
+ x: { display: false },
585
+ y: {
586
+ ticks: { color: '#71717a', font: { family: "'JetBrains Mono'", size: 10 } },
587
+ grid: { color: 'rgba(63,63,70,0.3)' },
588
+ border: { display: false },
589
+ }
590
+ },
591
+ elements: {
592
+ point: { radius: 0 },
593
+ line: { borderWidth: 1.5, tension: 0.3 },
594
+ }
595
+ };
596
+
597
+ function makeChart(id, color, minY, maxY) {
598
+ const ctx = document.getElementById(id).getContext('2d');
599
+ const opts = JSON.parse(JSON.stringify(chartOpts));
600
+ if (minY !== undefined) opts.scales.y.min = minY;
601
+ if (maxY !== undefined) opts.scales.y.max = maxY;
602
+ return new Chart(ctx, {
603
+ type: 'line',
604
+ data: {
605
+ labels: [],
606
+ datasets: [{
607
+ data: [],
608
+ borderColor: color,
609
+ backgroundColor: color + '18',
610
+ fill: true,
611
+ }]
612
+ },
613
+ options: opts,
614
+ });
615
+ }
616
+
617
+ function initCharts() {
618
+ chartHealth = makeChart('chart-health', '#22c55e', 0, 100);
619
+ chartGini = makeChart('chart-gini', '#eab308', 0, 1);
620
+ chartNetflow = makeChart('chart-netflow', '#3b82f6');
621
+ chartSatisfaction = makeChart('chart-satisfaction', '#22c55e', 0, 100);
622
+ }
623
+
624
+ function updateChart(chart, labels, data) {
625
+ chart.data.labels = labels;
626
+ chart.data.datasets[0].data = data;
627
+ chart.update('none');
628
+ }
629
+
630
+ // \u2500\u2500 Terminal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
631
+ function addTerminalLine(html) {
632
+ const el = document.createElement('div');
633
+ el.className = 'term-line';
634
+ el.innerHTML = html;
635
+ $terminal.appendChild(el);
636
+ while ($terminal.children.length > MAX_TERMINAL_LINES) {
637
+ $terminal.removeChild($terminal.firstChild);
638
+ }
639
+ $terminal.scrollTop = $terminal.scrollHeight;
640
+ }
641
+
642
+ function decisionToTerminal(d) {
643
+ const resultIcon = d.result === 'applied'
644
+ ? '<span class="t-ok">\\u2705 </span>'
645
+ : d.result === 'rejected'
646
+ ? '<span class="t-fail">\\u274c </span>'
647
+ : '<span class="t-skip">\\u23f8 </span>';
648
+
649
+ const principle = d.diagnosis?.principle || {};
650
+ const plan = d.plan || {};
651
+ const severity = d.diagnosis?.violation?.severity ?? '?';
652
+ const confidence = d.diagnosis?.violation?.confidence;
653
+ const confStr = confidence != null ? (confidence * 100).toFixed(0) + '%' : '?';
654
+
655
+ let advisorBtns = '';
656
+ if (isAdvisor && d.result === 'skipped_override') {
657
+ advisorBtns = '<span class="advisor-actions">'
658
+ + '<button class="advisor-btn approve" onclick="window._approve(\\'' + d.id + '\\')">[Approve]</button>'
659
+ + '<button class="advisor-btn reject" onclick="window._reject(\\'' + d.id + '\\')">[Reject]</button>'
660
+ + '</span>';
661
+ }
662
+
663
+ return '<span class="t-tick">[Tick ' + pad(d.tick) + ']</span> '
664
+ + resultIcon
665
+ + '<span class="t-principle">[' + (principle.id || '?') + '] ' + (principle.name || '') + ':</span> '
666
+ + '<span class="t-param">' + (plan.parameter || '\u2014') + ' </span>'
667
+ + '<span class="t-old">' + fmt(plan.currentValue) + '</span>'
668
+ + '<span class="t-arrow"> \\u2192 </span>'
669
+ + '<span class="t-new">' + fmt(plan.targetValue) + '</span>'
670
+ + '<span class="t-meta"> severity ' + severity + '/10, confidence ' + confStr + '</span>'
671
+ + advisorBtns;
672
+ }
673
+
674
+ // \u2500\u2500 Alerts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
675
+ function renderAlerts(alerts) {
676
+ if (!alerts || alerts.length === 0) {
677
+ $alertsContainer.innerHTML = '<div class="empty-state">No active violations</div>';
678
+ return;
679
+ }
680
+ const sorted = [...alerts].sort((a, b) => (b.severity || 0) - (a.severity || 0));
681
+ $alertsContainer.innerHTML = sorted.map(function(a) {
682
+ const sev = a.severity || a.violation?.severity || 0;
683
+ const sc = sevClass(sev);
684
+ const name = a.principleName || a.principle?.name || '?';
685
+ const pid = a.principleId || a.principle?.id || '?';
686
+ const reason = a.reasoning || a.violation?.suggestedAction?.reasoning || '';
687
+ return '<div class="alert-card">'
688
+ + '<span class="alert-severity ' + sc + '">' + sev + '/10</span>'
689
+ + '<div class="alert-body">'
690
+ + '<div class="alert-principle">[' + pid + '] ' + name + '</div>'
691
+ + '<div class="alert-reason">' + reason + '</div>'
692
+ + '</div></div>';
693
+ }).join('');
694
+ }
695
+
696
+ // \u2500\u2500 Violations table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
697
+ function addViolation(d) {
698
+ violations.push({
699
+ tick: d.tick,
700
+ principle: (d.diagnosis?.principle?.id || '?') + ' ' + (d.diagnosis?.principle?.name || ''),
701
+ severity: d.diagnosis?.violation?.severity || 0,
702
+ parameter: d.plan?.parameter || '\u2014',
703
+ result: d.result,
704
+ });
705
+ if (violations.length > MAX_VIOLATIONS) violations.shift();
706
+ renderViolations();
707
+ }
708
+
709
+ function renderViolations() {
710
+ const sorted = [...violations].sort(function(a, b) {
711
+ const va = a[violationSortKey], vb = b[violationSortKey];
712
+ if (va < vb) return violationSortAsc ? -1 : 1;
713
+ if (va > vb) return violationSortAsc ? 1 : -1;
714
+ return 0;
715
+ });
716
+ $violationsBody.innerHTML = sorted.map(function(v) {
717
+ return '<tr>'
718
+ + '<td>' + v.tick + '</td>'
719
+ + '<td style="color:var(--text-primary);font-family:var(--font-sans)">' + v.principle + '</td>'
720
+ + '<td><span class="alert-severity ' + sevClass(v.severity) + '">' + v.severity + '</span></td>'
721
+ + '<td>' + v.parameter + '</td>'
722
+ + '<td>' + v.result + '</td>'
723
+ + '</tr>';
724
+ }).join('');
725
+ }
726
+
727
+ // Table sorting
728
+ document.querySelectorAll('.violations-table th').forEach(function(th) {
729
+ th.addEventListener('click', function() {
730
+ const key = th.dataset.sort;
731
+ if (violationSortKey === key) violationSortAsc = !violationSortAsc;
732
+ else { violationSortKey = key; violationSortAsc = true; }
733
+ renderViolations();
734
+ });
735
+ });
736
+
737
+ // \u2500\u2500 Personas \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
738
+ function renderPersonas(dist) {
739
+ if (!dist || Object.keys(dist).length === 0) {
740
+ $personaBars.innerHTML = '<div class="empty-state">No persona data yet</div>';
741
+ return;
742
+ }
743
+ const total = Object.values(dist).reduce(function(s, v) { return s + v; }, 0);
744
+ const entries = Object.entries(dist).sort(function(a, b) { return b[1] - a[1]; });
745
+ $personaBars.innerHTML = entries.map(function(e) {
746
+ const pctVal = total > 0 ? (e[1] / total * 100) : 0;
747
+ return '<div class="persona-row">'
748
+ + '<div class="persona-label">' + e[0] + '</div>'
749
+ + '<div class="persona-bar-track"><div class="persona-bar-fill" style="width:' + pctVal + '%"></div></div>'
750
+ + '<div class="persona-pct">' + pctVal.toFixed(0) + '%</div>'
751
+ + '</div>';
752
+ }).join('');
753
+ }
754
+
755
+ // \u2500\u2500 Registry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
756
+ function renderRegistry(principles) {
757
+ if (!principles || principles.length === 0) {
758
+ $registryList.innerHTML = '<div class="empty-state">No parameters registered</div>';
759
+ return;
760
+ }
761
+ // Show unique parameters from principles
762
+ const params = new Set();
763
+ $registryList.innerHTML = principles.slice(0, 30).map(function(p) {
764
+ return '<div class="registry-item">'
765
+ + '<span class="registry-key">[' + p.id + ']</span>'
766
+ + '<span class="registry-val">' + p.name + '</span>'
767
+ + '</div>';
768
+ }).join('');
769
+ }
770
+
771
+ // \u2500\u2500 KPI update \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
772
+ function updateKPIs(data) {
773
+ if (data.health != null) {
774
+ $kpiHealth.textContent = data.health + '/100';
775
+ $kpiHealth.className = 'kpi-value ' + healthClass(data.health);
776
+ document.getElementById('cv-health').textContent = data.health + '/100';
777
+ }
778
+ if (data.mode != null) {
779
+ $kpiMode.textContent = data.mode;
780
+ isAdvisor = data.mode === 'advisor';
781
+ $app.classList.toggle('advisor-mode', isAdvisor);
782
+ }
783
+ if (data.tick != null) $kpiTick.textContent = data.tick;
784
+ if (data.uptime != null) $kpiUptime.textContent = formatUptime(data.uptime);
785
+ if (data.activePlans != null) $kpiPlans.textContent = data.activePlans;
786
+ }
787
+
788
+ // \u2500\u2500 Metrics history \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
789
+ function updateChartsFromHistory(history) {
790
+ if (!history || history.length === 0) return;
791
+ const ticks = history.map(function(h) { return h.tick; });
792
+ updateChart(chartHealth, ticks, history.map(function(h) { return h.health; }));
793
+ updateChart(chartGini, ticks, history.map(function(h) { return h.giniCoefficient; }));
794
+ updateChart(chartNetflow, ticks, history.map(function(h) { return h.netFlow; }));
795
+ updateChart(chartSatisfaction, ticks, history.map(function(h) { return h.avgSatisfaction; }));
796
+
797
+ const last = history[history.length - 1];
798
+ document.getElementById('cv-gini').textContent = last.giniCoefficient.toFixed(3);
799
+ document.getElementById('cv-netflow').textContent = last.netFlow.toFixed(1);
800
+ document.getElementById('cv-satisfaction').textContent = last.avgSatisfaction.toFixed(0) + '/100';
801
+ }
802
+
803
+ // \u2500\u2500 API calls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
804
+ function fetchJSON(path) {
805
+ return fetch(path).then(function(r) { return r.json(); });
806
+ }
807
+
808
+ function postJSON(path, body) {
809
+ return fetch(path, {
810
+ method: 'POST',
811
+ headers: { 'Content-Type': 'application/json' },
812
+ body: JSON.stringify(body),
813
+ }).then(function(r) { return r.json(); });
814
+ }
815
+
816
+ function loadInitialData() {
817
+ fetchJSON('/health').then(function(data) {
818
+ updateKPIs(data);
819
+ }).catch(function() {});
820
+
821
+ fetchJSON('/decisions?limit=50').then(function(data) {
822
+ if (data.decisions) {
823
+ data.decisions.reverse().forEach(function(d) {
824
+ addTerminalLine(decisionToTerminal(d));
825
+ addViolation(d);
826
+ });
827
+ }
828
+ }).catch(function() {});
829
+
830
+ fetchJSON('/metrics').then(function(data) {
831
+ if (data.history) updateChartsFromHistory(data.history);
832
+ if (data.latest) {
833
+ renderPersonas(data.latest.personaDistribution);
834
+ }
835
+ }).catch(function() {});
836
+
837
+ fetchJSON('/principles').then(function(data) {
838
+ if (data.principles) renderRegistry(data.principles);
839
+ }).catch(function() {});
840
+
841
+ fetchJSON('/pending').then(function(data) {
842
+ if (data.pending) {
843
+ pendingDecisions = data.pending;
844
+ $pendingCount.textContent = data.count || 0;
845
+ }
846
+ }).catch(function() {});
847
+ }
848
+
849
+ // \u2500\u2500 Polling fallback \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
850
+ let pollInterval = null;
851
+
852
+ function startPolling() {
853
+ if (pollInterval) return;
854
+ pollInterval = setInterval(function() {
855
+ fetchJSON('/health').then(updateKPIs).catch(function() {});
856
+ fetchJSON('/metrics').then(function(data) {
857
+ if (data.history) updateChartsFromHistory(data.history);
858
+ if (data.latest) renderPersonas(data.latest.personaDistribution);
859
+ }).catch(function() {});
860
+ }, 5000);
861
+ }
862
+
863
+ function stopPolling() {
864
+ if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
865
+ }
866
+
867
+ // \u2500\u2500 WebSocket \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
868
+ function connectWS() {
869
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
870
+ ws = new WebSocket(proto + '//' + location.host);
871
+
872
+ ws.onopen = function() {
873
+ reconnectDelay = 1000;
874
+ $liveDot.classList.remove('disconnected');
875
+ $liveDot.title = 'WebSocket connected';
876
+ stopPolling();
877
+ // Request fresh health
878
+ ws.send(JSON.stringify({ type: 'health' }));
879
+ };
880
+
881
+ ws.onclose = function() {
882
+ $liveDot.classList.add('disconnected');
883
+ $liveDot.title = 'WebSocket disconnected \u2014 reconnecting...';
884
+ startPolling();
885
+ setTimeout(connectWS, reconnectDelay);
886
+ reconnectDelay = Math.min(reconnectDelay * 1.5, MAX_RECONNECT);
887
+ };
888
+
889
+ ws.onerror = function() { ws.close(); };
890
+
891
+ ws.onmessage = function(ev) {
892
+ var msg;
893
+ try { msg = JSON.parse(ev.data); } catch(e) { return; }
894
+
895
+ switch (msg.type) {
896
+ case 'tick_result':
897
+ updateKPIs({ health: msg.health, tick: msg.tick });
898
+ if (msg.alerts) renderAlerts(msg.alerts);
899
+ // Refresh charts
900
+ fetchJSON('/metrics').then(function(data) {
901
+ if (data.history) updateChartsFromHistory(data.history);
902
+ if (data.latest) renderPersonas(data.latest.personaDistribution);
903
+ }).catch(function() {});
904
+ break;
905
+
906
+ case 'health_result':
907
+ updateKPIs(msg);
908
+ break;
909
+
910
+ case 'advisor_action':
911
+ if (msg.action === 'approved' || msg.action === 'rejected') {
912
+ pendingDecisions = pendingDecisions.filter(function(d) {
913
+ return d.id !== msg.decisionId;
914
+ });
915
+ $pendingCount.textContent = pendingDecisions.length;
916
+ }
917
+ break;
918
+ }
919
+ };
920
+ }
921
+
922
+ // \u2500\u2500 Advisor actions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
923
+ window._approve = function(id) {
924
+ postJSON('/approve', { decisionId: id }).then(function(data) {
925
+ if (data.ok) {
926
+ addTerminalLine('<span class="t-tick">[Advisor]</span> <span class="t-ok">\\u2705 Approved ' + id + '</span>');
927
+ }
928
+ }).catch(function() {});
929
+ };
930
+
931
+ window._reject = function(id) {
932
+ var reason = prompt('Rejection reason (optional):');
933
+ postJSON('/reject', { decisionId: id, reason: reason || undefined }).then(function(data) {
934
+ if (data.ok) {
935
+ addTerminalLine('<span class="t-tick">[Advisor]</span> <span class="t-fail">\\u274c Rejected ' + id + '</span>');
936
+ }
937
+ }).catch(function() {});
938
+ };
939
+
940
+ // \u2500\u2500 Init \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
941
+ initCharts();
942
+ loadInitialData();
943
+ connectWS();
944
+
945
+ })();
946
+ </script>
947
+ </body>
948
+ </html>`;
949
+ }
950
+
951
+ // src/routes.ts
952
+ function setCorsHeaders(res, origin) {
953
+ res.setHeader("Access-Control-Allow-Origin", origin);
954
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
955
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
956
+ }
957
+ function json(res, status, data, origin) {
958
+ setCorsHeaders(res, origin);
959
+ res.writeHead(status, { "Content-Type": "application/json" });
960
+ res.end(JSON.stringify(data));
961
+ }
962
+ var MAX_BODY_BYTES = 1048576;
963
+ function readBody(req) {
964
+ return new Promise((resolve, reject) => {
965
+ const chunks = [];
966
+ let totalBytes = 0;
967
+ req.on("data", (chunk) => {
968
+ totalBytes += chunk.length;
969
+ if (totalBytes > MAX_BODY_BYTES) {
970
+ req.destroy();
971
+ reject(new Error("Request body too large"));
972
+ return;
973
+ }
974
+ chunks.push(chunk);
975
+ });
976
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
977
+ req.on("error", reject);
978
+ });
979
+ }
980
+ function createRouteHandler(server) {
981
+ const cors = server.corsOrigin;
982
+ return async (req, res) => {
983
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
984
+ const path = url.pathname;
985
+ const method = req.method?.toUpperCase() ?? "GET";
986
+ if (method === "OPTIONS") {
987
+ setCorsHeaders(res, cors);
988
+ res.writeHead(204);
989
+ res.end();
990
+ return;
991
+ }
992
+ try {
993
+ if (path === "/tick" && method === "POST") {
994
+ const body = await readBody(req);
995
+ let parsed;
996
+ try {
997
+ parsed = JSON.parse(body);
998
+ } catch {
999
+ json(res, 400, { error: "Invalid JSON" }, cors);
1000
+ return;
1001
+ }
1002
+ if (!parsed || typeof parsed !== "object") {
1003
+ json(res, 400, { error: "Body must be a JSON object" }, cors);
1004
+ return;
1005
+ }
1006
+ const payload = parsed;
1007
+ const state = payload["state"] ?? parsed;
1008
+ const events = payload["events"];
1009
+ if (server.validateState) {
1010
+ const validation = validateEconomyState(state);
1011
+ if (!validation.valid) {
1012
+ json(res, 400, {
1013
+ error: "invalid_state",
1014
+ validationErrors: validation.errors
1015
+ }, cors);
1016
+ return;
1017
+ }
1018
+ }
1019
+ const result = await server.processTick(
1020
+ state,
1021
+ Array.isArray(events) ? events : void 0
1022
+ );
1023
+ const warnings = server.validateState ? validateEconomyState(state).warnings : [];
1024
+ json(res, 200, {
1025
+ adjustments: result.adjustments,
1026
+ alerts: result.alerts.map((a) => ({
1027
+ principleId: a.principle.id,
1028
+ principleName: a.principle.name,
1029
+ severity: a.violation.severity,
1030
+ evidence: a.violation.evidence,
1031
+ reasoning: a.violation.suggestedAction.reasoning
1032
+ })),
1033
+ health: result.health,
1034
+ tick: result.tick,
1035
+ ...warnings.length > 0 ? { validationWarnings: warnings } : {}
1036
+ }, cors);
1037
+ return;
1038
+ }
1039
+ if (path === "/health" && method === "GET") {
1040
+ const agentE = server.getAgentE();
1041
+ json(res, 200, {
1042
+ health: agentE.getHealth(),
1043
+ tick: agentE.metrics.latest()?.tick ?? 0,
1044
+ mode: agentE.getMode(),
1045
+ activePlans: agentE.getActivePlans().length,
1046
+ uptime: server.getUptime()
1047
+ }, cors);
1048
+ return;
1049
+ }
1050
+ if (path === "/decisions" && method === "GET") {
1051
+ const limit = parseInt(url.searchParams.get("limit") ?? "100", 10);
1052
+ const since = url.searchParams.get("since");
1053
+ const agentE = server.getAgentE();
1054
+ let decisions;
1055
+ if (since) {
1056
+ decisions = agentE.getDecisions({ since: parseInt(since, 10) });
1057
+ } else {
1058
+ decisions = agentE.log.latest(limit);
1059
+ }
1060
+ json(res, 200, { decisions }, cors);
1061
+ return;
1062
+ }
1063
+ if (path === "/config" && method === "POST") {
1064
+ const body = await readBody(req);
1065
+ let parsed;
1066
+ try {
1067
+ parsed = JSON.parse(body);
1068
+ } catch {
1069
+ json(res, 400, { error: "Invalid JSON" }, cors);
1070
+ return;
1071
+ }
1072
+ const config = parsed;
1073
+ if (Array.isArray(config["lock"])) {
1074
+ for (const param of config["lock"]) {
1075
+ if (typeof param === "string") server.lock(param);
1076
+ }
1077
+ }
1078
+ if (Array.isArray(config["unlock"])) {
1079
+ for (const param of config["unlock"]) {
1080
+ if (typeof param === "string") server.unlock(param);
1081
+ }
1082
+ }
1083
+ if (Array.isArray(config["constrain"])) {
1084
+ for (const c of config["constrain"]) {
1085
+ if (c && typeof c === "object" && typeof c["param"] === "string" && typeof c["min"] === "number" && typeof c["max"] === "number") {
1086
+ const constraint = c;
1087
+ server.constrain(constraint.param, { min: constraint.min, max: constraint.max });
1088
+ }
1089
+ }
1090
+ }
1091
+ if (config["mode"] === "autonomous" || config["mode"] === "advisor") {
1092
+ server.setMode(config["mode"]);
1093
+ }
1094
+ json(res, 200, { ok: true }, cors);
1095
+ return;
1096
+ }
1097
+ if (path === "/principles" && method === "GET") {
1098
+ const principles = server.getAgentE().getPrinciples();
1099
+ json(res, 200, {
1100
+ count: principles.length,
1101
+ principles: principles.map((p) => ({
1102
+ id: p.id,
1103
+ name: p.name,
1104
+ category: p.category,
1105
+ description: p.description
1106
+ }))
1107
+ }, cors);
1108
+ return;
1109
+ }
1110
+ if (path === "/diagnose" && method === "POST") {
1111
+ const body = await readBody(req);
1112
+ let parsed;
1113
+ try {
1114
+ parsed = JSON.parse(body);
1115
+ } catch {
1116
+ json(res, 400, { error: "Invalid JSON" }, cors);
1117
+ return;
1118
+ }
1119
+ const payload = parsed;
1120
+ const state = payload["state"] ?? parsed;
1121
+ if (server.validateState) {
1122
+ const validation = validateEconomyState(state);
1123
+ if (!validation.valid) {
1124
+ json(res, 400, { error: "invalid_state", validationErrors: validation.errors }, cors);
1125
+ return;
1126
+ }
1127
+ }
1128
+ const result = server.diagnoseOnly(state);
1129
+ json(res, 200, {
1130
+ health: result.health,
1131
+ diagnoses: result.diagnoses.map((d) => ({
1132
+ principleId: d.principle.id,
1133
+ principleName: d.principle.name,
1134
+ severity: d.violation.severity,
1135
+ evidence: d.violation.evidence,
1136
+ suggestedAction: d.violation.suggestedAction
1137
+ }))
1138
+ }, cors);
1139
+ return;
1140
+ }
1141
+ if (path === "/" && method === "GET" && server.serveDashboard) {
1142
+ setCorsHeaders(res, cors);
1143
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1144
+ res.end(getDashboardHtml());
1145
+ return;
1146
+ }
1147
+ if (path === "/metrics" && method === "GET") {
1148
+ const agentE = server.getAgentE();
1149
+ const latest = agentE.store.latest();
1150
+ const history = agentE.store.recentHistory(100);
1151
+ json(res, 200, { latest, history }, cors);
1152
+ return;
1153
+ }
1154
+ if (path === "/metrics/personas" && method === "GET") {
1155
+ const agentE = server.getAgentE();
1156
+ const latest = agentE.store.latest();
1157
+ const dist = latest.personaDistribution || {};
1158
+ const total = Object.values(dist).reduce((s, v) => s + v, 0);
1159
+ json(res, 200, { distribution: dist, total }, cors);
1160
+ return;
1161
+ }
1162
+ if (path === "/approve" && method === "POST") {
1163
+ const body = await readBody(req);
1164
+ let parsed;
1165
+ try {
1166
+ parsed = JSON.parse(body);
1167
+ } catch {
1168
+ json(res, 400, { error: "Invalid JSON" }, cors);
1169
+ return;
1170
+ }
1171
+ const payload = parsed;
1172
+ const decisionId = payload["decisionId"];
1173
+ if (!decisionId) {
1174
+ json(res, 400, { error: "missing_decision_id" }, cors);
1175
+ return;
1176
+ }
1177
+ const agentE = server.getAgentE();
1178
+ if (agentE.getMode() !== "advisor") {
1179
+ json(res, 400, { error: "not_in_advisor_mode" }, cors);
1180
+ return;
1181
+ }
1182
+ const entry = agentE.log.getById(decisionId);
1183
+ if (!entry) {
1184
+ json(res, 404, { error: "decision_not_found" }, cors);
1185
+ return;
1186
+ }
1187
+ if (entry.result !== "skipped_override") {
1188
+ json(res, 409, { error: "decision_not_pending", currentResult: entry.result }, cors);
1189
+ return;
1190
+ }
1191
+ await agentE.apply(entry.plan);
1192
+ agentE.log.updateResult(decisionId, "applied");
1193
+ server.broadcast({ type: "advisor_action", action: "approved", decisionId });
1194
+ json(res, 200, {
1195
+ ok: true,
1196
+ parameter: entry.plan.parameter,
1197
+ value: entry.plan.targetValue
1198
+ }, cors);
1199
+ return;
1200
+ }
1201
+ if (path === "/reject" && method === "POST") {
1202
+ const body = await readBody(req);
1203
+ let parsed;
1204
+ try {
1205
+ parsed = JSON.parse(body);
1206
+ } catch {
1207
+ json(res, 400, { error: "Invalid JSON" }, cors);
1208
+ return;
1209
+ }
1210
+ const payload = parsed;
1211
+ const decisionId = payload["decisionId"];
1212
+ const reason = payload["reason"] || void 0;
1213
+ if (!decisionId) {
1214
+ json(res, 400, { error: "missing_decision_id" }, cors);
1215
+ return;
1216
+ }
1217
+ const agentE = server.getAgentE();
1218
+ if (agentE.getMode() !== "advisor") {
1219
+ json(res, 400, { error: "not_in_advisor_mode" }, cors);
1220
+ return;
1221
+ }
1222
+ const entry = agentE.log.getById(decisionId);
1223
+ if (!entry) {
1224
+ json(res, 404, { error: "decision_not_found" }, cors);
1225
+ return;
1226
+ }
1227
+ if (entry.result !== "skipped_override") {
1228
+ json(res, 409, { error: "decision_not_pending", currentResult: entry.result }, cors);
1229
+ return;
1230
+ }
1231
+ agentE.log.updateResult(decisionId, "rejected", reason);
1232
+ server.broadcast({ type: "advisor_action", action: "rejected", decisionId, reason });
1233
+ json(res, 200, { ok: true, decisionId }, cors);
1234
+ return;
1235
+ }
1236
+ if (path === "/pending" && method === "GET") {
1237
+ const agentE = server.getAgentE();
1238
+ const pending = agentE.log.query({ result: "skipped_override" });
1239
+ json(res, 200, {
1240
+ mode: agentE.getMode(),
1241
+ pending,
1242
+ count: pending.length
1243
+ }, cors);
1244
+ return;
1245
+ }
1246
+ json(res, 404, { error: "Not found" }, cors);
1247
+ } catch (err) {
1248
+ console.error("[AgentE Server] Unhandled route error:", err);
1249
+ json(res, 500, { error: "Internal server error" }, cors);
1250
+ }
1251
+ };
1252
+ }
1253
+
1254
+ // src/websocket.ts
1255
+ import { WebSocketServer, WebSocket } from "ws";
1256
+ import { validateEconomyState as validateEconomyState2 } from "@agent-e/core";
1257
+ function send(ws, data) {
1258
+ if (ws.readyState === WebSocket.OPEN) {
1259
+ ws.send(JSON.stringify(data));
1260
+ }
1261
+ }
1262
+ function createWebSocketHandler(httpServer, server) {
1263
+ const wss = new WebSocketServer({ server: httpServer });
1264
+ const aliveMap = /* @__PURE__ */ new WeakMap();
1265
+ const heartbeatInterval = setInterval(() => {
1266
+ for (const ws of wss.clients) {
1267
+ if (ws.readyState === WebSocket.OPEN) {
1268
+ if (aliveMap.get(ws) === false) {
1269
+ ws.terminate();
1270
+ continue;
1271
+ }
1272
+ aliveMap.set(ws, false);
1273
+ ws.ping();
1274
+ }
1275
+ }
1276
+ }, 3e4);
1277
+ wss.on("connection", (ws) => {
1278
+ console.log("[AgentE Server] Client connected");
1279
+ aliveMap.set(ws, true);
1280
+ ws.on("pong", () => {
1281
+ aliveMap.set(ws, true);
1282
+ });
1283
+ ws.on("close", () => {
1284
+ console.log("[AgentE Server] Client disconnected");
1285
+ });
1286
+ ws.on("message", async (raw) => {
1287
+ let msg;
1288
+ try {
1289
+ msg = JSON.parse(raw.toString());
1290
+ } catch {
1291
+ send(ws, { type: "error", message: "Malformed JSON" });
1292
+ return;
1293
+ }
1294
+ if (!msg.type || typeof msg.type !== "string") {
1295
+ send(ws, { type: "error", message: 'Missing "type" field' });
1296
+ return;
1297
+ }
1298
+ switch (msg.type) {
1299
+ case "tick": {
1300
+ const state = msg["state"];
1301
+ const events = msg["events"];
1302
+ if (server.validateState) {
1303
+ const validation = validateEconomyState2(state);
1304
+ if (!validation.valid) {
1305
+ send(ws, { type: "validation_error", validationErrors: validation.errors });
1306
+ return;
1307
+ }
1308
+ if (validation.warnings.length > 0) {
1309
+ send(ws, { type: "validation_warning", validationWarnings: validation.warnings });
1310
+ }
1311
+ }
1312
+ try {
1313
+ const result = await server.processTick(
1314
+ state,
1315
+ Array.isArray(events) ? events : void 0
1316
+ );
1317
+ send(ws, {
1318
+ type: "tick_result",
1319
+ adjustments: result.adjustments,
1320
+ alerts: result.alerts.map((a) => ({
1321
+ principleId: a.principle.id,
1322
+ principleName: a.principle.name,
1323
+ severity: a.violation.severity,
1324
+ reasoning: a.violation.suggestedAction.reasoning
1325
+ })),
1326
+ health: result.health,
1327
+ tick: result.tick
1328
+ });
1329
+ } catch (err) {
1330
+ send(ws, { type: "error", message: "Tick processing failed" });
1331
+ }
1332
+ break;
1333
+ }
1334
+ case "event": {
1335
+ const event = msg["event"];
1336
+ if (event) {
1337
+ server.getAgentE().ingest(event);
1338
+ send(ws, { type: "event_ack" });
1339
+ } else {
1340
+ send(ws, { type: "error", message: 'Missing "event" field' });
1341
+ }
1342
+ break;
1343
+ }
1344
+ case "health": {
1345
+ const agentE = server.getAgentE();
1346
+ send(ws, {
1347
+ type: "health_result",
1348
+ health: agentE.getHealth(),
1349
+ tick: agentE.metrics.latest()?.tick ?? 0,
1350
+ mode: agentE.getMode(),
1351
+ activePlans: agentE.getActivePlans().length,
1352
+ uptime: server.getUptime()
1353
+ });
1354
+ break;
1355
+ }
1356
+ case "diagnose": {
1357
+ const state = msg["state"];
1358
+ if (server.validateState) {
1359
+ const validation = validateEconomyState2(state);
1360
+ if (!validation.valid) {
1361
+ send(ws, { type: "validation_error", validationErrors: validation.errors });
1362
+ return;
1363
+ }
1364
+ }
1365
+ const result = server.diagnoseOnly(state);
1366
+ send(ws, {
1367
+ type: "diagnose_result",
1368
+ health: result.health,
1369
+ diagnoses: result.diagnoses.map((d) => ({
1370
+ principleId: d.principle.id,
1371
+ principleName: d.principle.name,
1372
+ severity: d.violation.severity,
1373
+ suggestedAction: d.violation.suggestedAction
1374
+ }))
1375
+ });
1376
+ break;
1377
+ }
1378
+ default:
1379
+ send(ws, { type: "error", message: `Unknown message type: "${msg.type}"` });
1380
+ }
1381
+ });
1382
+ });
1383
+ function broadcast(data) {
1384
+ const payload = JSON.stringify(data);
1385
+ for (const ws of wss.clients) {
1386
+ if (ws.readyState === WebSocket.OPEN) {
1387
+ ws.send(payload);
1388
+ }
1389
+ }
1390
+ }
1391
+ return {
1392
+ cleanup: () => {
1393
+ clearInterval(heartbeatInterval);
1394
+ wss.close();
1395
+ },
1396
+ broadcast
1397
+ };
1398
+ }
1399
+
1400
+ // src/AgentEServer.ts
1401
+ var AgentEServer = class {
1402
+ constructor(config = {}) {
1403
+ this.lastState = null;
1404
+ this.adjustmentQueue = [];
1405
+ this.alerts = [];
1406
+ this.startedAt = Date.now();
1407
+ this.wsHandle = null;
1408
+ this.port = config.port ?? 3100;
1409
+ this.host = config.host ?? "0.0.0.0";
1410
+ this.validateState = config.validateState ?? true;
1411
+ this.corsOrigin = config.corsOrigin ?? "*";
1412
+ this.serveDashboard = config.serveDashboard ?? true;
1413
+ const adapter = {
1414
+ getState: () => {
1415
+ if (!this.lastState) {
1416
+ return {
1417
+ tick: 0,
1418
+ roles: [],
1419
+ resources: [],
1420
+ currencies: ["default"],
1421
+ agentBalances: {},
1422
+ agentRoles: {},
1423
+ agentInventories: {},
1424
+ marketPrices: {},
1425
+ recentTransactions: []
1426
+ };
1427
+ }
1428
+ return this.lastState;
1429
+ },
1430
+ setParam: (key, value, scope) => {
1431
+ this.adjustmentQueue.push({ key, value, scope });
1432
+ }
1433
+ };
1434
+ const agentECfg = config.agentE ?? {};
1435
+ const agentEConfig = {
1436
+ adapter,
1437
+ mode: agentECfg.mode ?? "autonomous",
1438
+ gracePeriod: agentECfg.gracePeriod ?? 0,
1439
+ checkInterval: agentECfg.checkInterval ?? 1,
1440
+ ...agentECfg.dominantRoles ? { dominantRoles: agentECfg.dominantRoles } : {},
1441
+ ...agentECfg.idealDistribution ? { idealDistribution: agentECfg.idealDistribution } : {},
1442
+ ...agentECfg.maxAdjustmentPercent !== void 0 ? { maxAdjustmentPercent: agentECfg.maxAdjustmentPercent } : {},
1443
+ ...agentECfg.cooldownTicks !== void 0 ? { cooldownTicks: agentECfg.cooldownTicks } : {},
1444
+ ...agentECfg.thresholds ? { thresholds: agentECfg.thresholds } : {}
1445
+ };
1446
+ this.thresholds = {
1447
+ ...DEFAULT_THRESHOLDS,
1448
+ ...agentECfg.thresholds ?? {},
1449
+ ...agentECfg.maxAdjustmentPercent !== void 0 ? { maxAdjustmentPercent: agentECfg.maxAdjustmentPercent } : {},
1450
+ ...agentECfg.cooldownTicks !== void 0 ? { cooldownTicks: agentECfg.cooldownTicks } : {}
1451
+ };
1452
+ this.agentE = new AgentE(agentEConfig);
1453
+ this.agentE.on("alert", (diagnosis) => {
1454
+ this.alerts.push(diagnosis);
1455
+ });
1456
+ this.agentE.connect(adapter).start();
1457
+ const routeHandler = createRouteHandler(this);
1458
+ this.server = http.createServer(routeHandler);
1459
+ }
1460
+ async start() {
1461
+ this.wsHandle = createWebSocketHandler(this.server, this);
1462
+ return new Promise((resolve) => {
1463
+ this.server.listen(this.port, this.host, () => {
1464
+ const addr = this.getAddress();
1465
+ console.log(`[AgentE Server] Listening on http://${addr.host}:${addr.port}`);
1466
+ resolve();
1467
+ });
1468
+ });
1469
+ }
1470
+ async stop() {
1471
+ this.agentE.stop();
1472
+ if (this.wsHandle) this.wsHandle.cleanup();
1473
+ return new Promise((resolve, reject) => {
1474
+ this.server.close((err) => {
1475
+ if (err) reject(err);
1476
+ else resolve();
1477
+ });
1478
+ });
1479
+ }
1480
+ getAgentE() {
1481
+ return this.agentE;
1482
+ }
1483
+ getAddress() {
1484
+ const addr = this.server.address();
1485
+ if (addr && typeof addr === "object") {
1486
+ return { port: addr.port, host: addr.address };
1487
+ }
1488
+ return { port: this.port, host: this.host };
1489
+ }
1490
+ getUptime() {
1491
+ return Date.now() - this.startedAt;
1492
+ }
1493
+ /**
1494
+ * Process a tick with the given state.
1495
+ * 1. Clear adjustment queue
1496
+ * 2. Set state
1497
+ * 3. Ingest events
1498
+ * 4. Run agentE.tick(state)
1499
+ * 5. Drain adjustment queue, enrich with reasoning from decisions
1500
+ * 6. Return response
1501
+ */
1502
+ async processTick(state, events) {
1503
+ this.adjustmentQueue = [];
1504
+ this.alerts = [];
1505
+ this.lastState = state;
1506
+ if (events) {
1507
+ for (const event of events) {
1508
+ this.agentE.ingest(event);
1509
+ }
1510
+ }
1511
+ await this.agentE.tick(state);
1512
+ const rawAdj = [...this.adjustmentQueue];
1513
+ this.adjustmentQueue = [];
1514
+ const decisions = this.agentE.getDecisions({ since: state.tick, until: state.tick });
1515
+ const adjustments = rawAdj.map((adj) => {
1516
+ const decision = decisions.find(
1517
+ (d) => d.plan.parameter === adj.key && d.result === "applied"
1518
+ );
1519
+ return {
1520
+ parameter: adj.key,
1521
+ value: adj.value,
1522
+ ...adj.scope ? { scope: adj.scope } : {},
1523
+ reasoning: decision?.diagnosis.violation.suggestedAction.reasoning ?? ""
1524
+ };
1525
+ });
1526
+ return {
1527
+ adjustments,
1528
+ alerts: [...this.alerts],
1529
+ health: this.agentE.getHealth(),
1530
+ tick: state.tick,
1531
+ decisions
1532
+ };
1533
+ }
1534
+ /**
1535
+ * Run Observer + Diagnoser on the given state without side effects (no execution).
1536
+ * Computes fresh metrics from the state rather than reading stored metrics.
1537
+ */
1538
+ diagnoseOnly(state) {
1539
+ const observer = new Observer();
1540
+ const diagnoser = new Diagnoser(ALL_PRINCIPLES);
1541
+ const metrics = observer.compute(state, []);
1542
+ const diagnoses = diagnoser.diagnose(metrics, this.thresholds);
1543
+ let health = 100;
1544
+ if (metrics.avgSatisfaction < 65) health -= 15;
1545
+ if (metrics.avgSatisfaction < 50) health -= 10;
1546
+ if (metrics.giniCoefficient > 0.45) health -= 15;
1547
+ if (metrics.giniCoefficient > 0.6) health -= 10;
1548
+ if (Math.abs(metrics.netFlow) > 10) health -= 15;
1549
+ if (Math.abs(metrics.netFlow) > 20) health -= 10;
1550
+ if (metrics.churnRate > 0.05) health -= 15;
1551
+ health = Math.max(0, Math.min(100, health));
1552
+ return { diagnoses, health };
1553
+ }
1554
+ setMode(mode) {
1555
+ this.agentE.setMode(mode);
1556
+ }
1557
+ lock(param) {
1558
+ this.agentE.lock(param);
1559
+ }
1560
+ unlock(param) {
1561
+ this.agentE.unlock(param);
1562
+ }
1563
+ constrain(param, bounds) {
1564
+ this.agentE.constrain(param, bounds);
1565
+ }
1566
+ broadcast(data) {
1567
+ if (this.wsHandle) this.wsHandle.broadcast(data);
1568
+ }
1569
+ };
1570
+
1571
+ export {
1572
+ AgentEServer
1573
+ };
1574
+ //# sourceMappingURL=chunk-OALXQFKY.mjs.map