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.
@@ -6,305 +6,614 @@
6
6
  <title>agents-harness</title>
7
7
  <style>
8
8
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
- body {
10
- font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
11
- background: #0d1117; color: #c9d1d9; line-height: 1.5; padding: 16px;
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;
12
14
  }
13
- header {
15
+ body { font-family: var(--font); background: var(--bg); color: var(--text); line-height: 1.5; }
16
+
17
+ /* === HEADER === */
18
+ .header {
14
19
  display: flex; justify-content: space-between; align-items: center;
15
- padding: 12px 16px; border-bottom: 1px solid #30363d; margin-bottom: 16px;
20
+ padding: 12px 20px; border-bottom: 1px solid var(--border);
16
21
  }
17
- header h1 { font-size: 16px; font-weight: 600; }
18
- .header-stats { display: flex; gap: 16px; font-size: 13px; color: #8b949e; }
19
- .header-stats span { color: #c9d1d9; }
22
+ .header-left { display: flex; align-items: center; gap: 10px; }
23
+ .header h1 { font-size: 15px; font-weight: 600; }
20
24
  #connection-dot {
21
- width: 8px; height: 8px; border-radius: 50%; display: inline-block;
22
- background: #f85149; margin-right: 8px; vertical-align: middle;
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;
23
61
  }
24
- #connection-dot.connected { background: #3fb950; }
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); }
25
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
+ }
26
84
  .sprint-card {
27
- background: #161b22; border: 1px solid #30363d; border-radius: 6px;
28
- padding: 12px 16px; margin-bottom: 8px; cursor: pointer; user-select: none;
29
- }
30
- .sprint-card:hover { border-color: #58a6ff; }
31
- .sprint-header {
32
- display: flex; align-items: center; gap: 12px; font-size: 14px;
33
- }
34
- .sprint-header .icon { font-size: 14px; width: 20px; text-align: center; }
35
- .sprint-header .name { font-weight: 600; }
36
- .sprint-header .meta { margin-left: auto; color: #8b949e; font-size: 12px; }
37
- .status-passed { color: #3fb950; }
38
- .status-failed { color: #f85149; }
39
- .status-progress { color: #d29922; }
40
-
41
- .sprint-details {
42
- display: none; margin-top: 10px; padding-top: 10px;
43
- border-top: 1px solid #30363d; font-size: 12px;
44
- }
45
- .sprint-details.open { display: block; }
46
- .criteria-list { padding-left: 8px; margin: 4px 0; }
47
- .criteria-list .pass { color: #3fb950; }
48
- .criteria-list .fail { color: #f85149; }
49
- .critique { color: #8b949e; margin-top: 6px; font-style: italic; }
50
-
51
- .activity-section {
52
- background: #161b22; border: 1px solid #30363d; border-radius: 6px;
53
- margin-top: 16px; max-height: 240px; overflow-y: auto;
54
- }
55
- .activity-section h2 {
56
- font-size: 13px; padding: 10px 16px; border-bottom: 1px solid #30363d;
57
- position: sticky; top: 0; background: #161b22; z-index: 1;
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;
58
88
  }
59
- .activity-entry {
60
- padding: 4px 16px; font-size: 12px; border-bottom: 1px solid #21262d;
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;
61
102
  }
62
- .activity-entry .time { color: #8b949e; margin-right: 8px; }
63
- .activity-entry .role { color: #58a6ff; margin-right: 4px; }
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; }
64
106
 
65
- .budget-bar {
66
- margin-top: 16px; background: #161b22; border: 1px solid #30363d;
67
- border-radius: 6px; padding: 10px 16px;
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;
68
114
  }
69
- .budget-bar .label {
70
- font-size: 12px; color: #8b949e; margin-bottom: 6px;
71
- display: flex; justify-content: space-between;
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);
72
142
  }
73
- .bar-track {
74
- height: 8px; background: #21262d; border-radius: 4px; overflow: hidden;
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);
75
149
  }
76
- .bar-fill {
77
- height: 100%; background: #58a6ff; border-radius: 4px;
78
- transition: width 0.3s ease;
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;
79
153
  }
154
+ .activity-entry .time { color: var(--text-dim); margin-right: 6px; }
155
+ .activity-entry .role { color: var(--blue); margin-right: 4px; }
80
156
 
81
- .run-complete-banner {
82
- text-align: center; padding: 16px; margin-top: 16px;
83
- border: 1px solid #30363d; border-radius: 6px; font-size: 14px;
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;
84
161
  }
85
- .run-complete-banner.completed { border-color: #3fb950; color: #3fb950; }
86
- .run-complete-banner.failed { border-color: #f85149; color: #f85149; }
87
- .run-complete-banner.stopped { border-color: #d29922; color: #d29922; }
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; }
88
164
 
89
- .empty-state {
90
- text-align: center; color: #8b949e; padding: 48px 16px; font-size: 14px;
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;
91
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); }
92
173
  </style>
93
174
  </head>
94
175
  <body>
95
- <header>
96
- <h1><span id="connection-dot"></span>agents-harness</h1>
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>
97
183
  <div class="header-stats">
98
184
  <div>Cost: <span id="total-cost">$0.00</span></div>
99
- <div>Duration: <span id="duration">0m 0s</span></div>
185
+ <div>Duration: <span id="duration">0s</span></div>
100
186
  </div>
101
- </header>
102
-
103
- <div id="sprints-container">
104
- <div class="empty-state" id="empty-state">Waiting for events...</div>
105
187
  </div>
106
188
 
107
- <div class="activity-section">
108
- <h2>Activity Stream</h2>
109
- <div id="activity-log"></div>
110
- </div>
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>
111
194
 
112
- <div class="budget-bar">
113
- <div class="label">
114
- <span>Budget</span>
115
- <span id="budget-text">$0.00 / $0.00</span>
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>
116
208
  </div>
117
- <div class="bar-track"><div class="bar-fill" id="budget-fill" style="width:0%"></div></div>
118
209
  </div>
119
210
 
120
- <div id="run-banner"></div>
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>
121
227
 
122
228
  <script>
123
229
  (function() {
124
- const state = {
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 = {
125
247
  sprints: {},
248
+ files: {},
249
+ activities: [],
126
250
  totalCost: 0,
127
251
  budget: 0,
128
- activities: [],
129
252
  startTime: Date.now(),
130
253
  runComplete: false,
131
- expanded: {},
254
+ currentPhase: null,
255
+ currentSprint: 0,
256
+ totalSprints: 0,
257
+ currentAttempt: 0,
258
+ selectedSprint: null,
259
+ activeTab: 'spec.md',
260
+ tabBadges: {},
261
+ activityOpen: false,
132
262
  };
133
263
 
134
- const MAX_ACTIVITIES = 50;
135
- let ws = null;
136
- let durationInterval = null;
137
-
138
- // --- WebSocket ---
139
- function connect() {
140
- const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
141
- ws = new WebSocket(`${proto}//${location.host}`);
142
- ws.onopen = () => {
143
- document.getElementById('connection-dot').classList.add('connected');
144
- };
145
- ws.onclose = () => {
146
- document.getElementById('connection-dot').classList.remove('connected');
147
- setTimeout(connect, 3000);
148
- };
149
- ws.onerror = () => { ws.close(); };
150
- ws.onmessage = (e) => {
151
- try { handleEvent(JSON.parse(e.data)); } catch {}
152
- };
153
- }
154
-
155
- function handleEvent(event) {
156
- const { type, data } = event;
157
- if (type === 'phase:start') onPhaseStart(data);
158
- else if (type === 'agent:activity') onActivity(data);
159
- else if (type === 'evaluation') onEvaluation(data);
160
- else if (type === 'cost:update') onCostUpdate(data);
161
- else if (type === 'sprint:complete') onSprintComplete(data);
162
- else if (type === 'run:complete') onRunComplete(data);
163
- }
164
-
165
- function ensureSprint(num) {
166
- if (!state.sprints[num]) {
167
- state.sprints[num] = { status: 'in_progress', attempts: 0, cost: 0, eval: null };
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>';
168
282
  }
169
- return state.sprints[num];
283
+ el.innerHTML = html;
170
284
  }
171
285
 
172
- function onPhaseStart(d) {
173
- if (d.sprint > 0) {
174
- const s = ensureSprint(d.sprint);
175
- s.attempts = Math.max(s.attempts, d.attempt || 1);
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';
176
293
  }
177
- renderSprints();
178
294
  }
179
295
 
180
- function onActivity(d) {
181
- state.activities.push(d);
182
- if (state.activities.length > MAX_ACTIVITIES) state.activities.shift();
183
- if (d.sprint > 0) ensureSprint(d.sprint);
184
- renderActivity();
185
- }
186
-
187
- function onEvaluation(d) {
188
- const s = ensureSprint(d.sprint);
189
- s.attempts = Math.max(s.attempts, d.attempt);
190
- s.eval = d.result;
191
- renderSprints();
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;
192
309
  }
193
310
 
194
- function onCostUpdate(d) {
195
- state.totalCost = d.totalCostUsd;
196
- state.budget = d.budgetUsd;
197
- document.getElementById('total-cost').textContent = `$${d.totalCostUsd.toFixed(2)}`;
198
- document.getElementById('budget-text').textContent =
199
- `$${d.totalCostUsd.toFixed(2)} / $${d.budgetUsd.toFixed(2)}`;
200
- const pct = d.budgetUsd > 0 ? Math.min(100, (d.totalCostUsd / d.budgetUsd) * 100) : 0;
201
- document.getElementById('budget-fill').style.width = `${pct}%`;
202
- }
311
+ window.__selectTab = function(key) {
312
+ state.activeTab = key;
313
+ state.tabBadges[key] = false;
314
+ renderFileTabs();
315
+ renderFileContent();
316
+ };
203
317
 
204
- function onSprintComplete(d) {
205
- const s = ensureSprint(d.sprint);
206
- s.status = d.status;
207
- s.attempts = d.attempts;
208
- s.cost = d.costUsd;
209
- renderSprints();
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
+ }
210
327
  }
211
328
 
212
- function onRunComplete(d) {
213
- state.runComplete = true;
214
- if (durationInterval) { clearInterval(durationInterval); durationInterval = null; }
215
- document.getElementById('duration').textContent = formatDuration(d.durationMs);
216
- document.getElementById('total-cost').textContent = `$${d.totalCostUsd.toFixed(2)}`;
217
-
218
- const banner = document.getElementById('run-banner');
219
- const status = d.status;
220
- banner.innerHTML = `<div class="run-complete-banner ${status}">Run ${status.toUpperCase()} &mdash; ${d.totalSprints} sprint${d.totalSprints !== 1 ? 's' : ''}, $${d.totalCostUsd.toFixed(2)}, ${formatDuration(d.durationMs)}</div>`;
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
+ }
221
337
  }
222
338
 
223
- // --- Rendering ---
339
+ // --- Sprint List ---
224
340
  function renderSprints() {
225
- const container = document.getElementById('sprints-container');
226
- const keys = Object.keys(state.sprints).map(Number).sort((a, b) => a - b);
341
+ var container = document.getElementById('sprint-list');
342
+ var keys = Object.keys(state.sprints).map(Number).sort(function(a,b){return a-b;});
227
343
  if (keys.length === 0) return;
228
344
 
229
- document.getElementById('empty-state')?.remove();
345
+ var emptyEl = document.getElementById('sprint-empty');
346
+ if (emptyEl) emptyEl.remove();
230
347
 
231
- let html = '';
232
- for (const num of keys) {
233
- const s = state.sprints[num];
234
- const icon = s.status === 'passed' ? 'PASS' : s.status === 'failed' ? 'FAIL' : '....';
235
- const cls = s.status === 'passed' ? 'status-passed' : s.status === 'failed' ? 'status-failed' : 'status-progress';
236
- const isOpen = state.expanded[num] ? 'open' : '';
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;';
237
355
 
238
- html += `<div class="sprint-card" onclick="window.__toggle(${num})">`;
239
- html += `<div class="sprint-header">`;
240
- html += `<span class="icon ${cls}">[${icon}]</span>`;
241
- html += `<span class="name">Sprint ${num}</span>`;
242
- html += `<span class="meta">${s.attempts} attempt${s.attempts !== 1 ? 's' : ''} &middot; $${s.cost.toFixed(2)}</span>`;
243
- html += `</div>`;
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>';
244
362
 
245
363
  if (s.eval) {
246
- html += `<div class="sprint-details ${isOpen}">`;
247
- if (s.eval.passedCriteria.length > 0) {
248
- html += `<div class="criteria-list">`;
249
- for (const c of s.eval.passedCriteria) html += `<div class="pass">+ ${esc(c)}</div>`;
250
- html += `</div>`;
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
+ }
251
369
  }
252
- if (s.eval.failedCriteria.length > 0) {
253
- html += `<div class="criteria-list">`;
254
- for (const c of s.eval.failedCriteria) html += `<div class="fail">- ${esc(c)}</div>`;
255
- html += `</div>`;
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
+ }
256
374
  }
257
375
  if (s.eval.critique) {
258
- html += `<div class="critique">${esc(s.eval.critique)}</div>`;
376
+ html += '<div class="critique-text">' + esc(s.eval.critique.slice(0, 200)) + '</div>';
259
377
  }
260
- html += `</div>`;
378
+ html += '</div>';
261
379
  }
262
- html += `</div>`;
380
+ html += '</div>';
263
381
  }
264
382
  container.innerHTML = html;
265
383
  }
266
384
 
267
- window.__toggle = function(num) {
268
- state.expanded[num] = !state.expanded[num];
385
+ window.__selectSprint = function(num) {
386
+ state.selectedSprint = num;
269
387
  renderSprints();
270
388
  };
271
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 ---
272
409
  function renderActivity() {
273
- const log = document.getElementById('activity-log');
274
- let html = '';
275
- for (const a of state.activities) {
276
- const t = new Date(a.timestamp).toLocaleTimeString();
277
- html += `<div class="activity-entry"><span class="time">${t}</span><span class="role">[${esc(a.role)}]</span> ${esc(a.summary)}</div>`;
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
+ }
278
464
  }
279
- log.innerHTML = html;
280
- log.scrollTop = log.scrollHeight;
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
+ };
281
589
  }
282
590
 
283
591
  // --- Helpers ---
284
592
  function esc(s) {
285
- const d = document.createElement('div');
593
+ if (!s) return '';
594
+ var d = document.createElement('div');
286
595
  d.textContent = s;
287
596
  return d.innerHTML;
288
597
  }
289
598
 
290
599
  function formatDuration(ms) {
291
- const s = Math.floor(ms / 1000);
292
- if (s < 60) return `${s}s`;
293
- const m = Math.floor(s / 60);
294
- const rs = s % 60;
295
- if (m < 60) return `${m}m ${rs}s`;
296
- const h = Math.floor(m / 60);
297
- const rm = m % 60;
298
- return `${h}h ${rm}m`;
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';
299
608
  }
300
609
 
301
610
  function updateDuration() {
302
611
  if (state.runComplete) return;
303
- const elapsed = Date.now() - state.startTime;
612
+ var elapsed = Date.now() - state.startTime;
304
613
  document.getElementById('duration').textContent = formatDuration(elapsed);
305
614
  }
306
615
 
307
- // --- Init ---
616
+ // --- Boot ---
308
617
  connect();
309
618
  durationInterval = setInterval(updateDuration, 1000);
310
619
  })();