@enderfga/claw-orchestrator 3.4.2 → 3.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +9 -9
  2. package/configs/autoloop-coder-prompt.md +59 -0
  3. package/configs/autoloop-planner-prompt.md +136 -0
  4. package/configs/autoloop-reviewer-prompt.md +76 -0
  5. package/dist/bin/cli.js +30 -4
  6. package/dist/bin/cli.js.map +1 -1
  7. package/dist/src/autoloop/agent-tools.d.ts +44 -0
  8. package/dist/src/autoloop/agent-tools.js +71 -0
  9. package/dist/src/autoloop/agent-tools.js.map +1 -0
  10. package/dist/src/autoloop/dispatcher.d.ts +137 -0
  11. package/dist/src/autoloop/dispatcher.js +580 -0
  12. package/dist/src/autoloop/dispatcher.js.map +1 -0
  13. package/dist/src/autoloop/messages.d.ts +114 -0
  14. package/dist/src/autoloop/messages.js +92 -0
  15. package/dist/src/autoloop/messages.js.map +1 -0
  16. package/dist/src/autoloop/notify.d.ts +36 -0
  17. package/dist/src/autoloop/notify.js +166 -0
  18. package/dist/src/autoloop/notify.js.map +1 -0
  19. package/dist/src/autoloop/planner-tools.d.ts +81 -0
  20. package/dist/src/autoloop/planner-tools.js +140 -0
  21. package/dist/src/autoloop/planner-tools.js.map +1 -0
  22. package/dist/src/autoloop/runner.d.ts +48 -0
  23. package/dist/src/autoloop/runner.js +214 -0
  24. package/dist/src/autoloop/runner.js.map +1 -0
  25. package/dist/src/autoloop/types.d.ts +69 -0
  26. package/dist/src/autoloop/types.js +16 -0
  27. package/dist/src/autoloop/types.js.map +1 -0
  28. package/dist/src/dashboard/index.html +910 -0
  29. package/dist/src/embedded-server.d.ts +1 -0
  30. package/dist/src/embedded-server.js +237 -51
  31. package/dist/src/embedded-server.js.map +1 -1
  32. package/dist/src/index.d.ts +5 -2
  33. package/dist/src/index.js +67 -105
  34. package/dist/src/index.js.map +1 -1
  35. package/dist/src/session-manager.d.ts +47 -14
  36. package/dist/src/session-manager.js +135 -47
  37. package/dist/src/session-manager.js.map +1 -1
  38. package/package.json +2 -2
  39. package/skills/references/autoloop.md +176 -193
  40. package/configs/autoloop-bootstrap-prompt.md +0 -30
  41. package/configs/autoloop-compress-prompt.md +0 -50
  42. package/configs/autoloop-propose-prompt.md +0 -57
  43. package/configs/autoloop-ratchet-prompt.md +0 -56
  44. package/dist/src/autoloop-types.d.ts +0 -218
  45. package/dist/src/autoloop-types.js +0 -130
  46. package/dist/src/autoloop-types.js.map +0 -1
  47. package/dist/src/autoloop.d.ts +0 -70
  48. package/dist/src/autoloop.js +0 -910
  49. package/dist/src/autoloop.js.map +0 -1
@@ -0,0 +1,910 @@
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" />
6
+ <title>Claw Orchestrator — Dashboard</title>
7
+ <style>
8
+ :root {
9
+ --bg: #f7f7f8;
10
+ --surface: #ffffff;
11
+ --surface-2: #f0f0f0;
12
+ --text: #1a1a1a;
13
+ --muted: #6e6e73;
14
+ --border: #e5e5e5;
15
+ --accent: #10a37f;
16
+ --accent-2: #cc785c;
17
+ --warn: #d97706;
18
+ --error: #dc2626;
19
+ --info: #2563eb;
20
+ }
21
+ * {
22
+ box-sizing: border-box;
23
+ }
24
+ html,
25
+ body {
26
+ height: 100%;
27
+ margin: 0;
28
+ background: var(--bg);
29
+ color: var(--text);
30
+ font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
31
+ }
32
+ a {
33
+ color: var(--info);
34
+ text-decoration: none;
35
+ }
36
+ a:hover {
37
+ text-decoration: underline;
38
+ }
39
+
40
+ .app {
41
+ display: flex;
42
+ flex-direction: column;
43
+ height: 100vh;
44
+ }
45
+ .header {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 16px;
49
+ padding: 10px 24px;
50
+ border-bottom: 1px solid var(--border);
51
+ background: var(--surface);
52
+ min-height: 52px;
53
+ }
54
+ .header h1 {
55
+ font-size: 16px;
56
+ font-weight: 700;
57
+ margin: 0;
58
+ letter-spacing: -0.3px;
59
+ }
60
+ .tabs {
61
+ display: flex;
62
+ gap: 6px;
63
+ margin-left: 12px;
64
+ }
65
+ .tab {
66
+ padding: 6px 12px;
67
+ border-radius: 6px;
68
+ border: 1px solid transparent;
69
+ background: transparent;
70
+ cursor: pointer;
71
+ font: inherit;
72
+ color: var(--muted);
73
+ }
74
+ .tab[aria-selected='true'] {
75
+ background: var(--surface-2);
76
+ color: var(--text);
77
+ border-color: var(--border);
78
+ }
79
+ .header .grow {
80
+ flex: 1;
81
+ }
82
+ .header small {
83
+ color: var(--muted);
84
+ }
85
+
86
+ .body {
87
+ display: flex;
88
+ flex: 1;
89
+ min-height: 0;
90
+ }
91
+
92
+ /* ── Sidebar (run / council list) ───────────────────────────── */
93
+ .sidebar {
94
+ width: 260px;
95
+ border-right: 1px solid var(--border);
96
+ background: var(--surface);
97
+ overflow-y: auto;
98
+ }
99
+ .sidebar h2 {
100
+ font-size: 11px;
101
+ font-weight: 600;
102
+ text-transform: uppercase;
103
+ letter-spacing: 0.05em;
104
+ color: var(--muted);
105
+ margin: 0;
106
+ padding: 14px 16px 6px;
107
+ }
108
+ .runlist {
109
+ list-style: none;
110
+ margin: 0;
111
+ padding: 0;
112
+ }
113
+ .runItem {
114
+ padding: 10px 16px;
115
+ border-bottom: 1px solid var(--border);
116
+ cursor: pointer;
117
+ }
118
+ .runItem:hover {
119
+ background: var(--surface-2);
120
+ }
121
+ .runItem[aria-selected='true'] {
122
+ background: var(--surface-2);
123
+ border-left: 3px solid var(--accent);
124
+ padding-left: 13px;
125
+ }
126
+ .runItem .id {
127
+ font-weight: 600;
128
+ font-size: 13px;
129
+ color: var(--text);
130
+ word-break: break-all;
131
+ }
132
+ .runItem .meta {
133
+ font-size: 11px;
134
+ color: var(--muted);
135
+ margin-top: 4px;
136
+ display: flex;
137
+ gap: 8px;
138
+ flex-wrap: wrap;
139
+ }
140
+ .badge {
141
+ display: inline-block;
142
+ padding: 1px 6px;
143
+ border-radius: 10px;
144
+ font-size: 11px;
145
+ font-weight: 600;
146
+ background: var(--surface-2);
147
+ color: var(--text);
148
+ }
149
+ .badge.running {
150
+ background: #dcfce7;
151
+ color: #166534;
152
+ }
153
+ .badge.paused {
154
+ background: #fef3c7;
155
+ color: #92400e;
156
+ }
157
+ .badge.terminated,
158
+ .badge.consensus,
159
+ .badge.accepted {
160
+ background: #e0e7ff;
161
+ color: #3730a3;
162
+ }
163
+ .badge.error,
164
+ .badge.crashed,
165
+ .badge.rejected,
166
+ .badge.max_rounds {
167
+ background: #fee2e2;
168
+ color: #991b1b;
169
+ }
170
+ .badge.planning,
171
+ .badge.awaiting_user {
172
+ background: #dbeafe;
173
+ color: #1e40af;
174
+ }
175
+
176
+ /* ── Main pane ──────────────────────────────────────────────── */
177
+ .main {
178
+ flex: 1;
179
+ display: flex;
180
+ flex-direction: column;
181
+ min-width: 0;
182
+ background: var(--bg);
183
+ }
184
+ .topbar {
185
+ background: var(--surface);
186
+ border-bottom: 1px solid var(--border);
187
+ padding: 10px 16px;
188
+ display: flex;
189
+ gap: 12px;
190
+ align-items: center;
191
+ flex-wrap: wrap;
192
+ font-size: 13px;
193
+ }
194
+ .topbar .item {
195
+ display: inline-flex;
196
+ align-items: center;
197
+ gap: 6px;
198
+ color: var(--muted);
199
+ }
200
+ .topbar .item strong {
201
+ color: var(--text);
202
+ }
203
+ .panes {
204
+ flex: 1;
205
+ display: grid;
206
+ grid-template-columns: 1fr 1fr 1fr;
207
+ min-height: 0;
208
+ gap: 1px;
209
+ background: var(--border);
210
+ }
211
+ .panes .single {
212
+ grid-column: 1 / -1;
213
+ }
214
+ .pane {
215
+ display: flex;
216
+ flex-direction: column;
217
+ min-height: 0;
218
+ background: var(--surface);
219
+ }
220
+ .paneHeader {
221
+ font-size: 11px;
222
+ text-transform: uppercase;
223
+ letter-spacing: 0.05em;
224
+ color: var(--muted);
225
+ font-weight: 600;
226
+ padding: 10px 16px;
227
+ border-bottom: 1px solid var(--border);
228
+ background: var(--surface);
229
+ }
230
+ .paneBody {
231
+ flex: 1;
232
+ overflow-y: auto;
233
+ padding: 10px 16px;
234
+ font-size: 13px;
235
+ line-height: 1.55;
236
+ }
237
+ .empty {
238
+ color: var(--muted);
239
+ font-style: italic;
240
+ padding: 20px;
241
+ text-align: center;
242
+ }
243
+
244
+ /* ── Chat / activity entries ────────────────────────────────── */
245
+ .entry {
246
+ margin-bottom: 14px;
247
+ padding-bottom: 10px;
248
+ border-bottom: 1px dashed var(--border);
249
+ }
250
+ .entry:last-child {
251
+ border-bottom: none;
252
+ }
253
+ .entryHeader {
254
+ display: flex;
255
+ gap: 8px;
256
+ align-items: baseline;
257
+ font-size: 11px;
258
+ color: var(--muted);
259
+ margin-bottom: 4px;
260
+ }
261
+ .entryHeader .who {
262
+ font-weight: 600;
263
+ color: var(--text);
264
+ }
265
+ .entryHeader .iter {
266
+ background: var(--surface-2);
267
+ padding: 1px 6px;
268
+ border-radius: 4px;
269
+ }
270
+ .entryBody {
271
+ white-space: pre-wrap;
272
+ word-wrap: break-word;
273
+ font-family:
274
+ ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
275
+ font-size: 12px;
276
+ background: #fafafa;
277
+ padding: 8px 10px;
278
+ border-radius: 4px;
279
+ border: 1px solid var(--border);
280
+ }
281
+ .entryBody.text {
282
+ font-family: inherit;
283
+ font-size: 13px;
284
+ background: transparent;
285
+ border: none;
286
+ padding: 0;
287
+ }
288
+ .entryBody.verdict-advance {
289
+ border-left: 3px solid var(--accent);
290
+ }
291
+ .entryBody.verdict-hold {
292
+ border-left: 3px solid var(--warn);
293
+ }
294
+ .entryBody.verdict-rollback {
295
+ border-left: 3px solid var(--error);
296
+ }
297
+
298
+ /* ── Chat compose for autoloop ──────────────────────────────── */
299
+ .composer {
300
+ display: flex;
301
+ gap: 8px;
302
+ padding: 10px 16px;
303
+ border-top: 1px solid var(--border);
304
+ background: var(--surface);
305
+ }
306
+ .composer textarea {
307
+ flex: 1;
308
+ font: inherit;
309
+ padding: 8px 10px;
310
+ border: 1px solid var(--border);
311
+ border-radius: 6px;
312
+ resize: vertical;
313
+ min-height: 36px;
314
+ max-height: 120px;
315
+ }
316
+ .composer button {
317
+ padding: 0 16px;
318
+ background: var(--accent);
319
+ color: white;
320
+ border: none;
321
+ border-radius: 6px;
322
+ font: inherit;
323
+ font-weight: 600;
324
+ cursor: pointer;
325
+ }
326
+ .composer button:disabled {
327
+ opacity: 0.5;
328
+ cursor: not-allowed;
329
+ }
330
+
331
+ /* ── Push log (bottom strip) ───────────────────────────────── */
332
+ .pushlog {
333
+ background: var(--surface);
334
+ border-top: 1px solid var(--border);
335
+ padding: 6px 16px;
336
+ font-size: 12px;
337
+ color: var(--muted);
338
+ display: flex;
339
+ gap: 12px;
340
+ flex-wrap: wrap;
341
+ max-height: 80px;
342
+ overflow-y: auto;
343
+ }
344
+ .pushlog .item {
345
+ white-space: nowrap;
346
+ }
347
+
348
+ .footer {
349
+ padding: 6px 16px;
350
+ font-size: 11px;
351
+ color: var(--muted);
352
+ text-align: right;
353
+ border-top: 1px solid var(--border);
354
+ background: var(--surface);
355
+ }
356
+ </style>
357
+ </head>
358
+ <body>
359
+ <div class="app">
360
+ <header class="header">
361
+ <h1>Claw Orchestrator</h1>
362
+ <nav class="tabs" role="tablist">
363
+ <button class="tab" role="tab" aria-selected="true" data-tab="autoloop">Autoloop</button>
364
+ <button class="tab" role="tab" aria-selected="false" data-tab="council">Council</button>
365
+ </nav>
366
+ <div class="grow"></div>
367
+ <small id="status">connected</small>
368
+ </header>
369
+
370
+ <div class="body">
371
+ <aside class="sidebar">
372
+ <h2 id="sidebarTitle">Runs</h2>
373
+ <ul class="runlist" id="runlist"></ul>
374
+ </aside>
375
+
376
+ <main class="main" id="main">
377
+ <div class="empty" style="margin-top: 80px">Select a run to view.</div>
378
+ </main>
379
+ </div>
380
+
381
+ <div class="footer">SSE-driven dashboard • served by claw-orchestrator embedded server</div>
382
+ </div>
383
+
384
+ <script>
385
+ // ─── State ────────────────────────────────────────────────────
386
+ const state = {
387
+ tab: 'autoloop',
388
+ selectedId: null,
389
+ runs: [],
390
+ // autoloop run-detail caches keyed by run_id
391
+ autoloop: {
392
+ state: null,
393
+ plannerLog: [],
394
+ coderLog: [],
395
+ reviewerLog: [],
396
+ pushLog: [],
397
+ eventSource: null,
398
+ },
399
+ // council session detail
400
+ council: {
401
+ session: null,
402
+ events: [],
403
+ eventSource: null,
404
+ },
405
+ };
406
+
407
+ const $ = (id) => document.getElementById(id);
408
+ const fmtTime = (iso) =>
409
+ iso ? new Date(iso).toLocaleTimeString(undefined, { hour12: false }) : '';
410
+ const escapeHtml = (s) =>
411
+ String(s ?? '').replace(/[&<>"']/g, (c) =>
412
+ ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c],
413
+ );
414
+
415
+ // ─── Tab switching ────────────────────────────────────────────
416
+ document.querySelectorAll('.tab').forEach((b) => {
417
+ b.addEventListener('click', () => switchTab(b.dataset.tab));
418
+ });
419
+
420
+ function switchTab(tab) {
421
+ if (state.tab === tab) return;
422
+ state.tab = tab;
423
+ document.querySelectorAll('.tab').forEach((b) => {
424
+ b.setAttribute('aria-selected', String(b.dataset.tab === tab));
425
+ });
426
+ $('sidebarTitle').textContent = tab === 'autoloop' ? 'Autoloop runs' : 'Council sessions';
427
+ state.selectedId = null;
428
+ closeStreams();
429
+ renderEmptyMain();
430
+ refreshList();
431
+ }
432
+
433
+ function closeStreams() {
434
+ if (state.autoloop.eventSource) {
435
+ state.autoloop.eventSource.close();
436
+ state.autoloop.eventSource = null;
437
+ }
438
+ if (state.council.eventSource) {
439
+ state.council.eventSource.close();
440
+ state.council.eventSource = null;
441
+ }
442
+ // Reset caches.
443
+ state.autoloop.state = null;
444
+ state.autoloop.plannerLog = [];
445
+ state.autoloop.coderLog = [];
446
+ state.autoloop.reviewerLog = [];
447
+ state.autoloop.pushLog = [];
448
+ state.council.session = null;
449
+ state.council.events = [];
450
+ }
451
+
452
+ function renderEmptyMain() {
453
+ $('main').innerHTML = '<div class="empty" style="margin-top:80px">Select a run to view.</div>';
454
+ }
455
+
456
+ // ─── List polling ─────────────────────────────────────────────
457
+ async function refreshList() {
458
+ try {
459
+ const url = state.tab === 'autoloop' ? '/autoloop/list' : '/council/list';
460
+ const r = await fetch(url);
461
+ const j = await r.json();
462
+ state.runs = state.tab === 'autoloop' ? j.runs || [] : j.councils || [];
463
+ renderList();
464
+ $('status').textContent = 'connected';
465
+ } catch (err) {
466
+ $('status').textContent = 'disconnected';
467
+ }
468
+ }
469
+ setInterval(refreshList, 5000);
470
+
471
+ function renderList() {
472
+ const ul = $('runlist');
473
+ if (state.runs.length === 0) {
474
+ ul.innerHTML =
475
+ '<li class="empty" style="padding:20px;list-style:none">No active ' +
476
+ (state.tab === 'autoloop' ? 'runs' : 'councils') +
477
+ '.</li>';
478
+ return;
479
+ }
480
+ const html = state.runs
481
+ .map((r) => {
482
+ if (state.tab === 'autoloop') {
483
+ return `
484
+ <li class="runItem" data-id="${escapeHtml(r.run_id)}" aria-selected="${r.run_id === state.selectedId}">
485
+ <div class="id">${escapeHtml(r.run_id)}</div>
486
+ <div class="meta">
487
+ <span class="badge ${r.status}">${r.status}</span>
488
+ <span>iter ${r.iter}</span>
489
+ <span>↑${r.push_log_count}</span>
490
+ </div>
491
+ </li>`;
492
+ } else {
493
+ return `
494
+ <li class="runItem" data-id="${escapeHtml(r.id)}" aria-selected="${r.id === state.selectedId}">
495
+ <div class="id">${escapeHtml(r.id)}</div>
496
+ <div class="meta">
497
+ <span class="badge ${r.status}">${r.status}</span>
498
+ <span>${r.responses?.length ?? 0} resp</span>
499
+ </div>
500
+ </li>`;
501
+ }
502
+ })
503
+ .join('');
504
+ ul.innerHTML = html;
505
+ ul.querySelectorAll('.runItem').forEach((el) => {
506
+ el.addEventListener('click', () => {
507
+ state.selectedId = el.dataset.id;
508
+ renderList();
509
+ openDetail(el.dataset.id);
510
+ });
511
+ });
512
+ }
513
+
514
+ // ─── Detail open ──────────────────────────────────────────────
515
+ function openDetail(id) {
516
+ closeStreams();
517
+ if (state.tab === 'autoloop') openAutoloop(id);
518
+ else openCouncil(id);
519
+ }
520
+
521
+ // ─── Autoloop detail ──────────────────────────────────────────
522
+ function renderAutoloopShell() {
523
+ $('main').innerHTML = `
524
+ <div class="topbar">
525
+ <div class="item">run <strong id="auto-id">…</strong></div>
526
+ <div class="item">status <strong id="auto-status">…</strong></div>
527
+ <div class="item">iter <strong id="auto-iter">0</strong></div>
528
+ <div class="item">subagents <strong id="auto-sub">no</strong></div>
529
+ <div class="item">pushes <strong id="auto-push">0</strong></div>
530
+ </div>
531
+ <div class="panes">
532
+ <section class="pane">
533
+ <div class="paneHeader">Planner ⇄ user</div>
534
+ <div class="paneBody" id="auto-planner"><div class="empty">Waiting…</div></div>
535
+ <div class="composer">
536
+ <textarea id="auto-input" placeholder="Send to planner… (Cmd/Ctrl+Enter)"></textarea>
537
+ <button id="auto-send">Send</button>
538
+ </div>
539
+ </section>
540
+ <section class="pane">
541
+ <div class="paneHeader">Coder activity</div>
542
+ <div class="paneBody" id="auto-coder"><div class="empty">Coder not yet spawned.</div></div>
543
+ </section>
544
+ <section class="pane">
545
+ <div class="paneHeader">Reviewer verdicts</div>
546
+ <div class="paneBody" id="auto-reviewer"><div class="empty">No verdicts yet.</div></div>
547
+ </section>
548
+ </div>
549
+ <div class="pushlog" id="auto-pushlog"><span class="item">No pushes yet.</span></div>
550
+ `;
551
+ $('auto-send').addEventListener('click', sendAutoloopChat);
552
+ $('auto-input').addEventListener('keydown', (e) => {
553
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') sendAutoloopChat();
554
+ });
555
+ }
556
+
557
+ async function openAutoloop(id) {
558
+ renderAutoloopShell();
559
+ $('auto-id').textContent = id;
560
+ // Initial state fetch
561
+ try {
562
+ const r = await fetch(`/autoloop/${encodeURIComponent(id)}/state`);
563
+ const j = await r.json();
564
+ if (j.ok) {
565
+ state.autoloop.state = j.state;
566
+ paintAutoloopState();
567
+ }
568
+ const logR = await fetch(`/autoloop/${encodeURIComponent(id)}/push_log`);
569
+ const logJ = await logR.json();
570
+ if (logJ.ok) {
571
+ state.autoloop.pushLog = logJ.entries || [];
572
+ paintPushLog();
573
+ }
574
+ } catch (err) {
575
+ /* ignore */
576
+ }
577
+ // SSE
578
+ const es = new EventSource(`/autoloop/${encodeURIComponent(id)}/events`);
579
+ state.autoloop.eventSource = es;
580
+ es.addEventListener('snapshot', (e) => {
581
+ const data = JSON.parse(e.data);
582
+ if (data.state) {
583
+ state.autoloop.state = data.state;
584
+ paintAutoloopState();
585
+ }
586
+ });
587
+ es.addEventListener('state', (e) => {
588
+ state.autoloop.state = JSON.parse(e.data);
589
+ paintAutoloopState();
590
+ });
591
+ es.addEventListener('planner_reply', (e) => {
592
+ const { text } = JSON.parse(e.data);
593
+ state.autoloop.plannerLog.push({ who: 'planner', text, ts: new Date().toISOString() });
594
+ paintPlanner();
595
+ });
596
+ es.addEventListener('coder_reply', (e) => {
597
+ const { text } = JSON.parse(e.data);
598
+ state.autoloop.coderLog.push({ who: 'coder', text, ts: new Date().toISOString() });
599
+ paintCoder();
600
+ });
601
+ es.addEventListener('reviewer_reply', (e) => {
602
+ const { text } = JSON.parse(e.data);
603
+ state.autoloop.reviewerLog.push({ who: 'reviewer', text, ts: new Date().toISOString() });
604
+ paintReviewer();
605
+ });
606
+ es.addEventListener('iter_done', (e) => {
607
+ const data = JSON.parse(e.data);
608
+ state.autoloop.reviewerLog.push({
609
+ who: 'iter_done',
610
+ text: `iter ${data.iter} → ${data.verdict}` + (data.metric != null ? ` · metric=${data.metric}` : ''),
611
+ verdict: data.verdict,
612
+ ts: new Date().toISOString(),
613
+ });
614
+ paintReviewer();
615
+ });
616
+ es.addEventListener('push', (e) => {
617
+ state.autoloop.pushLog.push({
618
+ ts: new Date().toISOString(),
619
+ ...JSON.parse(e.data),
620
+ });
621
+ paintPushLog();
622
+ });
623
+ es.addEventListener('terminated', (e) => {
624
+ const data = JSON.parse(e.data);
625
+ state.autoloop.plannerLog.push({
626
+ who: 'system',
627
+ text: `[terminated] ${data.reason ?? ''}`,
628
+ ts: new Date().toISOString(),
629
+ });
630
+ paintPlanner();
631
+ });
632
+ es.addEventListener('compact', (e) => {
633
+ const data = JSON.parse(e.data);
634
+ const entry = {
635
+ who: 'system',
636
+ text: `[auto-compact ${data.percent.toFixed(0)}% ≥ ${data.threshold}% — /compact dispatched]`,
637
+ ts: new Date().toISOString(),
638
+ };
639
+ if (data.agent === 'planner') {
640
+ state.autoloop.plannerLog.push(entry);
641
+ paintPlanner();
642
+ } else if (data.agent === 'coder') {
643
+ state.autoloop.coderLog.push(entry);
644
+ paintCoder();
645
+ } else if (data.agent === 'reviewer') {
646
+ state.autoloop.reviewerLog.push(entry);
647
+ paintReviewer();
648
+ }
649
+ });
650
+ }
651
+
652
+ function paintAutoloopState() {
653
+ const s = state.autoloop.state;
654
+ if (!s) return;
655
+ $('auto-status').innerHTML = `<span class="badge ${s.status}">${s.status}</span>`;
656
+ $('auto-iter').textContent = s.iter;
657
+ $('auto-sub').textContent = s.subagents_spawned ? 'yes' : 'no';
658
+ $('auto-push').textContent = s.push_log_count;
659
+ }
660
+
661
+ function paintPlanner() {
662
+ const el = $('auto-planner');
663
+ if (!el) return;
664
+ const items = state.autoloop.plannerLog;
665
+ if (!items.length) {
666
+ el.innerHTML = '<div class="empty">Waiting…</div>';
667
+ return;
668
+ }
669
+ el.innerHTML = items
670
+ .map(
671
+ (e) =>
672
+ `<div class="entry">
673
+ <div class="entryHeader"><span class="who">${escapeHtml(e.who)}</span><span>${fmtTime(e.ts)}</span></div>
674
+ <div class="entryBody text">${escapeHtml(e.text)}</div>
675
+ </div>`,
676
+ )
677
+ .join('');
678
+ el.scrollTop = el.scrollHeight;
679
+ }
680
+ function paintCoder() {
681
+ const el = $('auto-coder');
682
+ if (!el) return;
683
+ const items = state.autoloop.coderLog;
684
+ if (!items.length) {
685
+ el.innerHTML = '<div class="empty">Coder not yet spawned.</div>';
686
+ return;
687
+ }
688
+ el.innerHTML = items
689
+ .map(
690
+ (e) =>
691
+ `<div class="entry">
692
+ <div class="entryHeader"><span class="who">${escapeHtml(e.who)}</span><span>${fmtTime(e.ts)}</span></div>
693
+ <div class="entryBody">${escapeHtml(e.text)}</div>
694
+ </div>`,
695
+ )
696
+ .join('');
697
+ el.scrollTop = el.scrollHeight;
698
+ }
699
+ function paintReviewer() {
700
+ const el = $('auto-reviewer');
701
+ if (!el) return;
702
+ const items = state.autoloop.reviewerLog;
703
+ if (!items.length) {
704
+ el.innerHTML = '<div class="empty">No verdicts yet.</div>';
705
+ return;
706
+ }
707
+ el.innerHTML = items
708
+ .map((e) => {
709
+ const cls = e.verdict ? `verdict-${e.verdict}` : '';
710
+ return `<div class="entry">
711
+ <div class="entryHeader"><span class="who">${escapeHtml(e.who)}</span><span>${fmtTime(e.ts)}</span></div>
712
+ <div class="entryBody ${cls}">${escapeHtml(e.text)}</div>
713
+ </div>`;
714
+ })
715
+ .join('');
716
+ el.scrollTop = el.scrollHeight;
717
+ }
718
+ function paintPushLog() {
719
+ const el = $('auto-pushlog');
720
+ if (!el) return;
721
+ const items = state.autoloop.pushLog;
722
+ if (!items.length) {
723
+ el.innerHTML = '<span class="item">No pushes yet.</span>';
724
+ return;
725
+ }
726
+ el.innerHTML = items
727
+ .slice(-20)
728
+ .map(
729
+ (e) =>
730
+ `<span class="item">${fmtTime(e.ts)} <strong>${escapeHtml(e.level || '')}</strong> ${escapeHtml(e.summary || '')}</span>`,
731
+ )
732
+ .join(' · ');
733
+ }
734
+
735
+ async function sendAutoloopChat() {
736
+ const id = state.selectedId;
737
+ if (!id) return;
738
+ const ta = $('auto-input');
739
+ const text = ta.value.trim();
740
+ if (!text) return;
741
+ const btn = $('auto-send');
742
+ btn.disabled = true;
743
+ ta.disabled = true;
744
+ state.autoloop.plannerLog.push({ who: 'user', text, ts: new Date().toISOString() });
745
+ paintPlanner();
746
+ try {
747
+ await fetch('/v1/openclaw/tools/autoloop_chat', {
748
+ method: 'POST',
749
+ headers: { 'content-type': 'application/json' },
750
+ body: JSON.stringify({ run_id: id, text }),
751
+ });
752
+ ta.value = '';
753
+ } catch (err) {
754
+ state.autoloop.plannerLog.push({
755
+ who: 'system',
756
+ text: `[error] ${(err && err.message) || 'send failed'}`,
757
+ ts: new Date().toISOString(),
758
+ });
759
+ paintPlanner();
760
+ } finally {
761
+ btn.disabled = false;
762
+ ta.disabled = false;
763
+ ta.focus();
764
+ }
765
+ }
766
+
767
+ // ─── Council detail ───────────────────────────────────────────
768
+ function renderCouncilShell() {
769
+ $('main').innerHTML = `
770
+ <div class="topbar">
771
+ <div class="item">council <strong id="c-id">…</strong></div>
772
+ <div class="item">status <strong id="c-status">…</strong></div>
773
+ <div class="item">round <strong id="c-round">0</strong></div>
774
+ <div class="item">agents <strong id="c-agents">…</strong></div>
775
+ <div class="item">task <strong id="c-task" style="font-weight:400;color:var(--muted)"></strong></div>
776
+ </div>
777
+ <div class="panes">
778
+ <section class="pane single">
779
+ <div class="paneHeader">Agent responses (live)</div>
780
+ <div class="paneBody" id="c-stream"><div class="empty">Waiting for events…</div></div>
781
+ </section>
782
+ </div>
783
+ `;
784
+ }
785
+
786
+ async function openCouncil(id) {
787
+ renderCouncilShell();
788
+ $('c-id').textContent = id;
789
+ try {
790
+ const r = await fetch(`/council/${encodeURIComponent(id)}/state`);
791
+ const j = await r.json();
792
+ if (j.ok) {
793
+ state.council.session = j.session;
794
+ paintCouncilState();
795
+ // Replay existing responses as initial entries.
796
+ (j.session.responses || []).forEach((resp) => {
797
+ state.council.events.push({
798
+ ts: resp.timestamp,
799
+ who: resp.agent,
800
+ round: resp.round,
801
+ text: resp.content,
802
+ consensus: resp.consensus,
803
+ });
804
+ });
805
+ paintCouncilStream();
806
+ }
807
+ } catch (err) {
808
+ /* ignore */
809
+ }
810
+ const es = new EventSource(`/council/${encodeURIComponent(id)}/events`);
811
+ state.council.eventSource = es;
812
+ es.addEventListener('snapshot', (e) => {
813
+ state.council.session = JSON.parse(e.data);
814
+ paintCouncilState();
815
+ });
816
+ es.addEventListener('council-event', (e) => {
817
+ const ev = JSON.parse(e.data);
818
+ handleCouncilEvent(ev);
819
+ });
820
+ }
821
+
822
+ function paintCouncilState() {
823
+ const s = state.council.session;
824
+ if (!s) return;
825
+ $('c-status').innerHTML = `<span class="badge ${s.status}">${s.status}</span>`;
826
+ const rounds =
827
+ s.responses && s.responses.length > 0
828
+ ? Math.max(...s.responses.map((r) => r.round))
829
+ : 0;
830
+ $('c-round').textContent = rounds;
831
+ $('c-agents').textContent = (s.config?.agents || []).map((a) => a.name).join(', ');
832
+ $('c-task').textContent = s.task || '';
833
+ }
834
+
835
+ function handleCouncilEvent(ev) {
836
+ // Materialise interesting events as stream entries.
837
+ const ts = ev.timestamp || new Date().toISOString();
838
+ switch (ev.type) {
839
+ case 'session-start':
840
+ state.council.events.push({ ts, who: 'system', text: `[session-start] ${ev.task || ''}` });
841
+ break;
842
+ case 'round-start':
843
+ state.council.events.push({ ts, who: 'system', text: `── round ${ev.round} ──` });
844
+ break;
845
+ case 'agent-start':
846
+ state.council.events.push({ ts, who: ev.agent, round: ev.round, text: '(thinking…)' });
847
+ break;
848
+ case 'agent-complete':
849
+ state.council.events.push({
850
+ ts,
851
+ who: ev.agent,
852
+ round: ev.round,
853
+ text: ev.content || '',
854
+ consensus: ev.consensus,
855
+ });
856
+ break;
857
+ case 'round-end':
858
+ state.council.events.push({ ts, who: 'system', text: `[round ${ev.round} → ${ev.status}]` });
859
+ break;
860
+ case 'complete':
861
+ state.council.events.push({ ts, who: 'system', text: `[complete: ${ev.status}]` });
862
+ break;
863
+ case 'error':
864
+ state.council.events.push({
865
+ ts,
866
+ who: 'system',
867
+ text: `[error${ev.agent ? ' ' + ev.agent : ''}] ${ev.error || ''}`,
868
+ });
869
+ break;
870
+ default:
871
+ return; // skip chunks/tools at this resolution
872
+ }
873
+ // Update rounds count from latest event with a round
874
+ if (ev.round && state.council.session) {
875
+ const cur = state.council.session;
876
+ cur.responses = cur.responses || [];
877
+ paintCouncilState();
878
+ }
879
+ paintCouncilStream();
880
+ }
881
+
882
+ function paintCouncilStream() {
883
+ const el = $('c-stream');
884
+ if (!el) return;
885
+ const items = state.council.events;
886
+ if (!items.length) {
887
+ el.innerHTML = '<div class="empty">Waiting for events…</div>';
888
+ return;
889
+ }
890
+ el.innerHTML = items
891
+ .map((e) => {
892
+ const tag = e.consensus ? ' (consensus ✓)' : '';
893
+ const round = e.round != null ? ` <span class="iter">r${e.round}</span>` : '';
894
+ return `<div class="entry">
895
+ <div class="entryHeader">
896
+ <span class="who">${escapeHtml(e.who || '?')}</span>${round}
897
+ <span>${fmtTime(e.ts)}${escapeHtml(tag)}</span>
898
+ </div>
899
+ <div class="entryBody text">${escapeHtml(e.text || '')}</div>
900
+ </div>`;
901
+ })
902
+ .join('');
903
+ el.scrollTop = el.scrollHeight;
904
+ }
905
+
906
+ // ─── Boot ────────────────────────────────────────────────────
907
+ refreshList();
908
+ </script>
909
+ </body>
910
+ </html>