agents-harness 0.3.0 → 0.3.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-harness",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Multi-agent orchestrator for autonomous software development",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,7 +8,7 @@
8
8
  "agents-harness": "dist/cli.js"
9
9
  },
10
10
  "scripts": {
11
- "build": "tsc && cp -r src/dashboard/static dist/dashboard/static",
11
+ "build": "tsc && rm -rf dist/dashboard/static && cp -r src/dashboard/static dist/dashboard/static",
12
12
  "dev": "tsc --watch",
13
13
  "test": "vitest run",
14
14
  "test:watch": "vitest",
File without changes
@@ -1,622 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>agents-harness</title>
7
- <style>
8
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
- :root {
10
- --bg: #0d1117; --bg-card: #161b22; --border: #30363d; --border-hover: #58a6ff;
11
- --text: #c9d1d9; --text-dim: #8b949e; --text-bright: #f0f6fc;
12
- --blue: #58a6ff; --green: #3fb950; --red: #f85149; --yellow: #d29922;
13
- --font: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
14
- }
15
- body { font-family: var(--font); background: var(--bg); color: var(--text); line-height: 1.5; }
16
-
17
- /* === HEADER === */
18
- .header {
19
- display: flex; justify-content: space-between; align-items: center;
20
- padding: 12px 20px; border-bottom: 1px solid var(--border);
21
- }
22
- .header-left { display: flex; align-items: center; gap: 10px; }
23
- .header h1 { font-size: 15px; font-weight: 600; }
24
- #connection-dot {
25
- width: 8px; height: 8px; border-radius: 50%;
26
- background: var(--red); display: inline-block;
27
- }
28
- #connection-dot.connected { background: var(--green); }
29
- .header-stats { display: flex; gap: 16px; font-size: 12px; color: var(--text-dim); }
30
- .header-stats span { color: var(--text); }
31
-
32
- /* === PHASE PIPELINE === */
33
- .pipeline {
34
- display: flex; align-items: center; gap: 0; padding: 14px 20px;
35
- border-bottom: 1px solid var(--border); overflow-x: auto;
36
- }
37
- .phase-step {
38
- display: flex; align-items: center; gap: 0; white-space: nowrap;
39
- }
40
- .phase-dot {
41
- width: 28px; height: 28px; border-radius: 50%; display: flex;
42
- align-items: center; justify-content: center; font-size: 10px; font-weight: 700;
43
- border: 2px solid var(--border); color: var(--text-dim); background: var(--bg);
44
- transition: all 0.3s;
45
- }
46
- .phase-label {
47
- font-size: 11px; color: var(--text-dim); margin-left: 6px; margin-right: 6px;
48
- transition: color 0.3s;
49
- }
50
- .phase-connector {
51
- width: 24px; height: 2px; background: var(--border); margin: 0 2px;
52
- transition: background 0.3s;
53
- }
54
- .phase-step.done .phase-dot { border-color: var(--green); color: var(--green); background: rgba(63,185,80,0.1); }
55
- .phase-step.done .phase-label { color: var(--green); }
56
- .phase-step.done + .phase-step .phase-connector,
57
- .phase-step.done .phase-connector { background: var(--green); }
58
- .phase-step.active .phase-dot {
59
- border-color: var(--blue); color: var(--blue); background: rgba(88,166,255,0.15);
60
- animation: pulse 1.5s ease-in-out infinite;
61
- }
62
- .phase-step.active .phase-label { color: var(--blue); font-weight: 600; }
63
- @keyframes pulse {
64
- 0%, 100% { box-shadow: 0 0 0 0 rgba(88,166,255,0.4); }
65
- 50% { box-shadow: 0 0 0 6px rgba(88,166,255,0); }
66
- }
67
-
68
- .sprint-label {
69
- padding: 6px 20px; font-size: 12px; color: var(--text-dim);
70
- border-bottom: 1px solid var(--border);
71
- }
72
- .sprint-label strong { color: var(--text); }
73
-
74
- /* === MAIN SPLIT === */
75
- .main {
76
- display: grid; grid-template-columns: 320px 1fr;
77
- height: calc(100vh - 170px); min-height: 300px;
78
- }
79
-
80
- /* === LEFT: SPRINT LIST === */
81
- .sprint-list {
82
- border-right: 1px solid var(--border); overflow-y: auto; padding: 8px;
83
- }
84
- .sprint-card {
85
- background: var(--bg-card); border: 1px solid var(--border); border-radius: 6px;
86
- padding: 10px 12px; margin-bottom: 6px; cursor: pointer; user-select: none;
87
- transition: border-color 0.2s;
88
- }
89
- .sprint-card:hover { border-color: var(--border-hover); }
90
- .sprint-card.selected { border-color: var(--blue); border-left: 3px solid var(--blue); }
91
- .sprint-card-header { display: flex; align-items: center; gap: 8px; font-size: 13px; }
92
- .sprint-icon { font-size: 13px; width: 18px; text-align: center; }
93
- .sprint-icon.passed { color: var(--green); }
94
- .sprint-icon.failed { color: var(--red); }
95
- .sprint-icon.in_progress { color: var(--yellow); }
96
- .sprint-icon.pending { color: var(--text-dim); }
97
- .sprint-name { font-weight: 600; font-size: 13px; }
98
- .sprint-meta { margin-left: auto; color: var(--text-dim); font-size: 11px; }
99
-
100
- .sprint-eval {
101
- margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border); font-size: 11px;
102
- }
103
- .crit-pass { color: var(--green); }
104
- .crit-fail { color: var(--red); }
105
- .critique-text { color: var(--text-dim); font-style: italic; margin-top: 4px; }
106
-
107
- .empty-state { text-align: center; color: var(--text-dim); padding: 40px 12px; font-size: 13px; }
108
-
109
- /* === RIGHT: FILE VIEWER === */
110
- .file-viewer { display: flex; flex-direction: column; overflow: hidden; }
111
- .file-tabs {
112
- display: flex; border-bottom: 1px solid var(--border); background: var(--bg-card);
113
- overflow-x: auto; flex-shrink: 0;
114
- }
115
- .file-tab {
116
- padding: 8px 14px; font-size: 12px; color: var(--text-dim); cursor: pointer;
117
- border-bottom: 2px solid transparent; white-space: nowrap; position: relative;
118
- transition: color 0.2s, border-color 0.2s; user-select: none;
119
- }
120
- .file-tab:hover { color: var(--text); }
121
- .file-tab.active { color: var(--blue); border-bottom-color: var(--blue); }
122
- .file-tab .badge {
123
- width: 6px; height: 6px; border-radius: 50%; background: var(--blue);
124
- position: absolute; top: 6px; right: 6px; display: none;
125
- }
126
- .file-tab .badge.show { display: block; }
127
- .file-content {
128
- flex: 1; overflow-y: auto; padding: 16px 20px; font-size: 12px;
129
- white-space: pre-wrap; word-wrap: break-word; line-height: 1.6;
130
- color: var(--text);
131
- }
132
- .file-content .empty-file { color: var(--text-dim); font-style: italic; }
133
-
134
- /* === BOTTOM: ACTIVITY + BUDGET === */
135
- .bottom-bar {
136
- border-top: 1px solid var(--border);
137
- }
138
- .activity-toggle {
139
- padding: 8px 20px; font-size: 12px; color: var(--text-dim); cursor: pointer;
140
- display: flex; align-items: center; gap: 6px; user-select: none;
141
- border-bottom: 1px solid var(--border);
142
- }
143
- .activity-toggle:hover { color: var(--text); }
144
- .activity-toggle .arrow { transition: transform 0.2s; display: inline-block; }
145
- .activity-toggle .arrow.open { transform: rotate(90deg); }
146
- .activity-stream {
147
- max-height: 0; overflow: hidden; transition: max-height 0.3s ease;
148
- background: var(--bg-card);
149
- }
150
- .activity-stream.open { max-height: 200px; overflow-y: auto; }
151
- .activity-entry {
152
- padding: 3px 20px; font-size: 11px; border-bottom: 1px solid #21262d;
153
- }
154
- .activity-entry .time { color: var(--text-dim); margin-right: 6px; }
155
- .activity-entry .role { color: var(--blue); margin-right: 4px; }
156
-
157
- .budget-bar { padding: 8px 20px; }
158
- .budget-label {
159
- font-size: 11px; color: var(--text-dim); margin-bottom: 4px;
160
- display: flex; justify-content: space-between;
161
- }
162
- .bar-track { height: 6px; background: #21262d; border-radius: 3px; overflow: hidden; }
163
- .bar-fill { height: 100%; background: var(--blue); border-radius: 3px; transition: width 0.3s ease; }
164
-
165
- .run-banner {
166
- text-align: center; padding: 12px; margin: 8px 20px; font-size: 13px;
167
- border: 1px solid var(--border); border-radius: 6px; display: none;
168
- }
169
- .run-banner.show { display: block; }
170
- .run-banner.completed { border-color: var(--green); color: var(--green); }
171
- .run-banner.failed { border-color: var(--red); color: var(--red); }
172
- .run-banner.stopped { border-color: var(--yellow); color: var(--yellow); }
173
- </style>
174
- </head>
175
- <body>
176
-
177
- <!-- Header -->
178
- <div class="header">
179
- <div class="header-left">
180
- <span id="connection-dot"></span>
181
- <h1>agents-harness</h1>
182
- </div>
183
- <div class="header-stats">
184
- <div>Cost: <span id="total-cost">$0.00</span></div>
185
- <div>Duration: <span id="duration">0s</span></div>
186
- </div>
187
- </div>
188
-
189
- <!-- Phase Pipeline -->
190
- <div class="pipeline" id="pipeline"></div>
191
-
192
- <!-- Sprint Label -->
193
- <div class="sprint-label" id="sprint-label">Waiting for run to start...</div>
194
-
195
- <!-- Main Split -->
196
- <div class="main">
197
- <!-- Left: Sprint List -->
198
- <div class="sprint-list" id="sprint-list">
199
- <div class="empty-state" id="sprint-empty">No sprints yet</div>
200
- </div>
201
-
202
- <!-- Right: File Viewer -->
203
- <div class="file-viewer">
204
- <div class="file-tabs" id="file-tabs"></div>
205
- <div class="file-content" id="file-content">
206
- <span class="empty-file">Select a file tab to view contents</span>
207
- </div>
208
- </div>
209
- </div>
210
-
211
- <!-- Bottom -->
212
- <div class="bottom-bar">
213
- <div class="activity-toggle" id="activity-toggle">
214
- <span class="arrow" id="activity-arrow">&#9654;</span> Activity Stream
215
- <span id="activity-count" style="margin-left:auto;font-size:11px"></span>
216
- </div>
217
- <div class="activity-stream" id="activity-stream"></div>
218
- <div class="budget-bar">
219
- <div class="budget-label">
220
- <span>Budget</span>
221
- <span id="budget-text">$0.00 / $0.00</span>
222
- </div>
223
- <div class="bar-track"><div class="bar-fill" id="budget-fill" style="width:0%"></div></div>
224
- </div>
225
- <div class="run-banner" id="run-banner"></div>
226
- </div>
227
-
228
- <script>
229
- (function() {
230
- // --- Constants ---
231
- var PHASES = ['plan', 'decompose', 'contract', 'generate', 'evaluate', 'handoff'];
232
- var FILE_TABS = [
233
- { key: 'spec.md', label: 'Spec' },
234
- { key: 'sprints.md', label: 'Sprints' },
235
- { key: 'contract.md', label: 'Contract' },
236
- { key: 'evaluation.md', label: 'Evaluation' },
237
- { key: 'handoff.md', label: 'Handoff' },
238
- ];
239
- var PHASE_TO_TAB = {
240
- plan: 'spec.md', decompose: 'sprints.md', contract: 'contract.md',
241
- generate: 'contract.md', evaluate: 'evaluation.md', handoff: 'handoff.md',
242
- };
243
- var MAX_ACTIVITIES = 100;
244
-
245
- // --- State ---
246
- var state = {
247
- sprints: {},
248
- files: {},
249
- activities: [],
250
- totalCost: 0,
251
- budget: 0,
252
- startTime: Date.now(),
253
- runComplete: false,
254
- currentPhase: null,
255
- currentSprint: 0,
256
- totalSprints: 0,
257
- currentAttempt: 0,
258
- selectedSprint: null,
259
- activeTab: 'spec.md',
260
- tabBadges: {},
261
- activityOpen: false,
262
- };
263
-
264
- var ws = null;
265
- var durationInterval = null;
266
-
267
- // --- Init DOM ---
268
- buildPipeline();
269
- buildFileTabs();
270
-
271
- // --- Pipeline ---
272
- function buildPipeline() {
273
- var el = document.getElementById('pipeline');
274
- var html = '';
275
- for (var i = 0; i < PHASES.length; i++) {
276
- var p = PHASES[i];
277
- if (i > 0) html += '<div class="phase-connector" id="conn-' + i + '"></div>';
278
- html += '<div class="phase-step" id="phase-' + p + '">';
279
- html += '<div class="phase-dot">' + (i + 1) + '</div>';
280
- html += '<span class="phase-label">' + p.charAt(0).toUpperCase() + p.slice(1) + '</span>';
281
- html += '</div>';
282
- }
283
- el.innerHTML = html;
284
- }
285
-
286
- function updatePipeline() {
287
- var activeIdx = PHASES.indexOf(state.currentPhase);
288
- for (var i = 0; i < PHASES.length; i++) {
289
- var el = document.getElementById('phase-' + PHASES[i]);
290
- el.className = 'phase-step';
291
- if (i < activeIdx) el.className += ' done';
292
- else if (i === activeIdx) el.className += ' active';
293
- }
294
- }
295
-
296
- // --- File Tabs ---
297
- function buildFileTabs() {
298
- var container = document.getElementById('file-tabs');
299
- var html = '';
300
- for (var i = 0; i < FILE_TABS.length; i++) {
301
- var t = FILE_TABS[i];
302
- var cls = t.key === state.activeTab ? ' active' : '';
303
- html += '<div class="file-tab' + cls + '" data-key="' + t.key + '" onclick="window.__selectTab(\'' + t.key + '\')">';
304
- html += t.label;
305
- html += '<span class="badge" id="badge-' + t.key.replace('.', '-') + '"></span>';
306
- html += '</div>';
307
- }
308
- container.innerHTML = html;
309
- }
310
-
311
- window.__selectTab = function(key) {
312
- state.activeTab = key;
313
- state.tabBadges[key] = false;
314
- renderFileTabs();
315
- renderFileContent();
316
- };
317
-
318
- function renderFileTabs() {
319
- var tabs = document.getElementById('file-tabs').children;
320
- for (var i = 0; i < tabs.length; i++) {
321
- var tab = tabs[i];
322
- var key = tab.getAttribute('data-key');
323
- tab.className = 'file-tab' + (key === state.activeTab ? ' active' : '');
324
- var badge = tab.querySelector('.badge');
325
- if (badge) badge.className = 'badge' + (state.tabBadges[key] ? ' show' : '');
326
- }
327
- }
328
-
329
- function renderFileContent() {
330
- var el = document.getElementById('file-content');
331
- var content = state.files[state.activeTab];
332
- if (content) {
333
- el.textContent = content;
334
- } else {
335
- el.innerHTML = '<span class="empty-file">No content yet — file will appear when the agent writes to it</span>';
336
- }
337
- }
338
-
339
- // --- Sprint List ---
340
- function renderSprints() {
341
- var container = document.getElementById('sprint-list');
342
- var keys = Object.keys(state.sprints).map(Number).sort(function(a,b){return a-b;});
343
- if (keys.length === 0) return;
344
-
345
- var emptyEl = document.getElementById('sprint-empty');
346
- if (emptyEl) emptyEl.remove();
347
-
348
- var html = '';
349
- for (var i = 0; i < keys.length; i++) {
350
- var num = keys[i];
351
- var s = state.sprints[num];
352
- var sel = state.selectedSprint === num ? ' selected' : '';
353
- var iconCls = s.status || 'pending';
354
- var iconChar = s.status === 'passed' ? '&#10003;' : s.status === 'failed' ? '&#10007;' : s.status === 'in_progress' ? '&#9679;' : '&#9675;';
355
-
356
- html += '<div class="sprint-card' + sel + '" onclick="window.__selectSprint(' + num + ')">';
357
- html += '<div class="sprint-card-header">';
358
- html += '<span class="sprint-icon ' + iconCls + '">' + iconChar + '</span>';
359
- html += '<span class="sprint-name">Sprint ' + num + '</span>';
360
- html += '<span class="sprint-meta">' + (s.attempts || 0) + ' att &middot; $' + (s.cost || 0).toFixed(2) + '</span>';
361
- html += '</div>';
362
-
363
- if (s.eval) {
364
- html += '<div class="sprint-eval">';
365
- if (s.eval.passedCriteria && s.eval.passedCriteria.length > 0) {
366
- for (var j = 0; j < s.eval.passedCriteria.length; j++) {
367
- html += '<div class="crit-pass">+ ' + esc(s.eval.passedCriteria[j]) + '</div>';
368
- }
369
- }
370
- if (s.eval.failedCriteria && s.eval.failedCriteria.length > 0) {
371
- for (var j = 0; j < s.eval.failedCriteria.length; j++) {
372
- html += '<div class="crit-fail">- ' + esc(s.eval.failedCriteria[j]) + '</div>';
373
- }
374
- }
375
- if (s.eval.critique) {
376
- html += '<div class="critique-text">' + esc(s.eval.critique.slice(0, 200)) + '</div>';
377
- }
378
- html += '</div>';
379
- }
380
- html += '</div>';
381
- }
382
- container.innerHTML = html;
383
- }
384
-
385
- window.__selectSprint = function(num) {
386
- state.selectedSprint = num;
387
- renderSprints();
388
- };
389
-
390
- // --- Sprint Label ---
391
- function updateSprintLabel() {
392
- var el = document.getElementById('sprint-label');
393
- if (state.currentSprint > 0) {
394
- var parts = ['<strong>Sprint ' + state.currentSprint + '</strong>'];
395
- if (state.totalSprints > 0) parts[0] += ' of ' + state.totalSprints;
396
- if (state.currentAttempt > 0) parts.push('Attempt ' + state.currentAttempt);
397
- if (state.currentPhase) parts.push(state.currentPhase.charAt(0).toUpperCase() + state.currentPhase.slice(1) + ' phase');
398
- el.innerHTML = parts.join(' &mdash; ');
399
- } else if (state.currentPhase) {
400
- el.innerHTML = '<strong>' + state.currentPhase.charAt(0).toUpperCase() + state.currentPhase.slice(1) + '</strong> phase';
401
- } else if (state.runComplete) {
402
- el.innerHTML = 'Run complete';
403
- } else {
404
- el.innerHTML = 'Waiting for run to start...';
405
- }
406
- }
407
-
408
- // --- Activity ---
409
- function renderActivity() {
410
- var el = document.getElementById('activity-stream');
411
- var html = '';
412
- for (var i = 0; i < state.activities.length; i++) {
413
- var a = state.activities[i];
414
- var t = new Date(a.timestamp).toLocaleTimeString();
415
- html += '<div class="activity-entry">';
416
- html += '<span class="time">' + t + '</span>';
417
- html += '<span class="role">[' + esc(a.role) + ']</span> ';
418
- html += esc(a.summary);
419
- html += '</div>';
420
- }
421
- el.innerHTML = html;
422
- el.scrollTop = el.scrollHeight;
423
- document.getElementById('activity-count').textContent = state.activities.length > 0 ? '(' + state.activities.length + ')' : '';
424
- }
425
-
426
- document.getElementById('activity-toggle').addEventListener('click', function() {
427
- state.activityOpen = !state.activityOpen;
428
- document.getElementById('activity-stream').className = 'activity-stream' + (state.activityOpen ? ' open' : '');
429
- document.getElementById('activity-arrow').className = 'arrow' + (state.activityOpen ? ' open' : '');
430
- });
431
-
432
- // --- Event Handlers ---
433
- function ensureSprint(num) {
434
- if (!state.sprints[num]) {
435
- state.sprints[num] = { status: 'in_progress', attempts: 0, cost: 0, eval: null };
436
- }
437
- return state.sprints[num];
438
- }
439
-
440
- function handleEvent(event) {
441
- var type = event.type;
442
- var data = event.data;
443
-
444
- if (type === 'phase:start') {
445
- state.currentPhase = data.phase;
446
- state.currentSprint = data.sprint;
447
- state.currentAttempt = data.attempt;
448
- if (data.sprint > 0) {
449
- var s = ensureSprint(data.sprint);
450
- s.attempts = Math.max(s.attempts, data.attempt || 1);
451
- if (!state.selectedSprint) state.selectedSprint = data.sprint;
452
- }
453
- updatePipeline();
454
- updateSprintLabel();
455
- renderSprints();
456
- // Auto-switch tab
457
- var tabKey = PHASE_TO_TAB[data.phase];
458
- if (tabKey) {
459
- state.activeTab = tabKey;
460
- state.tabBadges[tabKey] = false;
461
- renderFileTabs();
462
- renderFileContent();
463
- }
464
- }
465
- else if (type === 'agent:activity') {
466
- state.activities.push(data);
467
- if (state.activities.length > MAX_ACTIVITIES) state.activities.shift();
468
- if (data.sprint > 0) ensureSprint(data.sprint);
469
- renderActivity();
470
- }
471
- else if (type === 'evaluation') {
472
- var s = ensureSprint(data.sprint);
473
- s.attempts = Math.max(s.attempts, data.attempt);
474
- s.eval = data.result;
475
- renderSprints();
476
- }
477
- else if (type === 'cost:update') {
478
- state.totalCost = data.totalCostUsd;
479
- state.budget = data.budgetUsd;
480
- document.getElementById('total-cost').textContent = '$' + data.totalCostUsd.toFixed(2);
481
- document.getElementById('budget-text').textContent =
482
- '$' + data.totalCostUsd.toFixed(2) + ' / $' + data.budgetUsd.toFixed(2);
483
- var pct = data.budgetUsd > 0 ? Math.min(100, (data.totalCostUsd / data.budgetUsd) * 100) : 0;
484
- document.getElementById('budget-fill').style.width = pct + '%';
485
- }
486
- else if (type === 'sprint:complete') {
487
- var s = ensureSprint(data.sprint);
488
- s.status = data.status;
489
- s.attempts = data.attempts;
490
- s.cost = data.costUsd;
491
- renderSprints();
492
- }
493
- else if (type === 'run:complete') {
494
- state.runComplete = true;
495
- if (durationInterval) { clearInterval(durationInterval); durationInterval = null; }
496
- document.getElementById('duration').textContent = formatDuration(data.durationMs);
497
- document.getElementById('total-cost').textContent = '$' + data.totalCostUsd.toFixed(2);
498
- updateSprintLabel();
499
-
500
- var banner = document.getElementById('run-banner');
501
- banner.className = 'run-banner show ' + data.status;
502
- banner.textContent = 'Run ' + data.status.toUpperCase() +
503
- ' \u2014 ' + data.totalSprints + ' sprint' + (data.totalSprints !== 1 ? 's' : '') +
504
- ', $' + data.totalCostUsd.toFixed(2) +
505
- ', ' + formatDuration(data.durationMs);
506
- }
507
- else if (type === 'file:update') {
508
- state.files[data.name] = data.content;
509
- if (data.name === state.activeTab) {
510
- renderFileContent();
511
- } else {
512
- state.tabBadges[data.name] = true;
513
- renderFileTabs();
514
- }
515
- }
516
- else if (type === 'state:snapshot') {
517
- // Restore files
518
- if (data.files) {
519
- for (var key in data.files) {
520
- if (data.files[key] !== null) {
521
- state.files[key] = data.files[key];
522
- }
523
- }
524
- }
525
- // Replay persisted events to rebuild full state (sprints, activities, cost, etc.)
526
- if (data.events && data.events.length > 0) {
527
- for (var i = 0; i < data.events.length; i++) {
528
- handleEvent(data.events[i]);
529
- }
530
- }
531
- // Restore progress (override with latest from progress file)
532
- if (data.progress) {
533
- var p = data.progress;
534
- state.currentPhase = p.currentPhase;
535
- state.currentSprint = p.currentSprint;
536
- state.totalSprints = p.totalSprints;
537
- state.currentAttempt = p.currentAttempt;
538
- state.totalCost = p.costUsd || 0;
539
- state.budget = p.maxBudgetUsd || 0;
540
- if (p.sprints) {
541
- for (var sn in p.sprints) {
542
- var existing = state.sprints[sn];
543
- state.sprints[sn] = {
544
- status: p.sprints[sn].status,
545
- attempts: p.sprints[sn].attempts,
546
- cost: p.sprints[sn].costUsd || 0,
547
- eval: existing ? existing.eval : null,
548
- };
549
- }
550
- }
551
- if (p.status === 'completed' || p.status === 'failed' || p.status === 'stopped') {
552
- state.runComplete = true;
553
- }
554
- }
555
- // Re-render everything
556
- updatePipeline();
557
- updateSprintLabel();
558
- renderSprints();
559
- renderFileTabs();
560
- renderFileContent();
561
- renderActivity();
562
- if (state.totalCost > 0) {
563
- document.getElementById('total-cost').textContent = '$' + state.totalCost.toFixed(2);
564
- }
565
- if (state.budget > 0) {
566
- document.getElementById('budget-text').textContent =
567
- '$' + state.totalCost.toFixed(2) + ' / $' + state.budget.toFixed(2);
568
- var pct = Math.min(100, (state.totalCost / state.budget) * 100);
569
- document.getElementById('budget-fill').style.width = pct + '%';
570
- }
571
- }
572
- }
573
-
574
- // --- WebSocket ---
575
- function connect() {
576
- var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
577
- ws = new WebSocket(proto + '//' + location.host);
578
- ws.onopen = function() {
579
- document.getElementById('connection-dot').classList.add('connected');
580
- };
581
- ws.onclose = function() {
582
- document.getElementById('connection-dot').classList.remove('connected');
583
- setTimeout(connect, 3000);
584
- };
585
- ws.onerror = function() { ws.close(); };
586
- ws.onmessage = function(e) {
587
- try { handleEvent(JSON.parse(e.data)); } catch(err) { console.error('WS parse error', err); }
588
- };
589
- }
590
-
591
- // --- Helpers ---
592
- function esc(s) {
593
- if (!s) return '';
594
- var d = document.createElement('div');
595
- d.textContent = s;
596
- return d.innerHTML;
597
- }
598
-
599
- function formatDuration(ms) {
600
- var s = Math.floor(ms / 1000);
601
- if (s < 60) return s + 's';
602
- var m = Math.floor(s / 60);
603
- var rs = s % 60;
604
- if (m < 60) return m + 'm ' + rs + 's';
605
- var h = Math.floor(m / 60);
606
- var rm = m % 60;
607
- return h + 'h ' + rm + 'm';
608
- }
609
-
610
- function updateDuration() {
611
- if (state.runComplete) return;
612
- var elapsed = Date.now() - state.startTime;
613
- document.getElementById('duration').textContent = formatDuration(elapsed);
614
- }
615
-
616
- // --- Boot ---
617
- connect();
618
- durationInterval = setInterval(updateDuration, 1000);
619
- })();
620
- </script>
621
- </body>
622
- </html>