File size: 24,398 Bytes
a8613ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7627e69
a8613ac
7627e69
a8613ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7627e69
a8613ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
/* RSI Lab: skill-level recursive self-improvement under the ViKernel kill switch.
   The agent edits its own skill (prompt, tools, workflow). No weight training happens in this demo. */
(function () {
  const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
  const api = (p, b) => window.VK_API(p, b === undefined ? undefined : Object.assign({session: window.VK_SID}, b));
  const errMsg = e => (e && e.message === 'rate') ? 'Too many requests. Wait a few seconds and try again.' : 'Request failed: ' + esc(e && e.message || 'network');
  const sleep = ms => new Promise(r => setTimeout(r, ms));
  const ICON = '<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 1-15.5 6.2"/><path d="M3 12a9 9 0 0 1 15.5-6.2"/><polyline points="18.5 2 18.5 6 14.5 6"/><polyline points="5.5 22 5.5 18 9.5 18"/></svg>';

  const css = `
.vr-root{--vr-bg:#f8fafc;--vr-fg:#0f172a;--vr-mut:#64748b;--vr-line:#e2e8f0;--vr-card:#fff;--vr-soft:#f1f5f9;--vr-acc:#9333EA;
  --vr-ok:#16a34a;--vr-bad:#dc2626;--vr-warn:#d97706;--vr-addbg:#dcfce7;--vr-delbg:#fee2e2;
  background:var(--vr-bg);color:var(--vr-fg);font:13px/1.45 system-ui,-apple-system,Segoe UI,sans-serif;height:100%;overflow:auto;box-sizing:border-box;padding:14px}
[data-theme=dark] .vr-root{--vr-bg:#0f172a;--vr-fg:#e2e8f0;--vr-mut:#94a3b8;--vr-line:#334155;--vr-card:#1e293b;--vr-soft:#172033;--vr-addbg:#14532d;--vr-delbg:#7f1d1d}
@media (prefers-color-scheme: dark){:root:not([data-theme=light]) .vr-root{--vr-bg:#0f172a;--vr-fg:#e2e8f0;--vr-mut:#94a3b8;--vr-line:#334155;--vr-card:#1e293b;--vr-soft:#172033;--vr-addbg:#14532d;--vr-delbg:#7f1d1d}}
.vr-root *{box-sizing:border-box}
.vr-head{display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin-bottom:12px}
.vr-title{flex:1 1 320px;min-width:0}.vr-title h2{margin:0;font-size:17px}.vr-title p{margin:2px 0 0;color:var(--vr-mut)}
.vr-gen{font:800 30px/1 ui-monospace,Consolas,monospace;color:var(--vr-acc);padding:6px 12px;border:2px solid var(--vr-acc);border-radius:12px;min-width:110px;text-align:center}
.vr-gen.bump{animation:vrbump .5s ease}@keyframes vrbump{40%{transform:scale(1.15)}}
.vr-ks{display:flex;gap:6px;align-items:center;flex-wrap:wrap}
.vr-pill{font-weight:700;padding:4px 10px;border-radius:999px;font-size:12px;background:var(--vr-soft);border:1px solid var(--vr-line)}
.vr-pill.run{background:#dcfce7;color:#166534;border-color:#86efac}.vr-pill.halt{background:#fef3c7;color:#92400e;border-color:#f59e0b;animation:vrblink 1s infinite}
@keyframes vrblink{50%{opacity:.6}}
.vr-btn{border:1px solid var(--vr-line);background:var(--vr-card);color:var(--vr-fg);border-radius:8px;padding:7px 11px;font:inherit;font-weight:600;cursor:pointer}
.vr-btn:hover:not(:disabled){border-color:var(--vr-acc)}.vr-btn:disabled{opacity:.5;cursor:not-allowed}
.vr-btn:focus-visible,.vr-in:focus-visible{outline:2px solid var(--vr-acc);outline-offset:2px}
.vr-btn.pri{background:var(--vr-acc);border-color:var(--vr-acc);color:#fff}.vr-btn.kill{background:#dc2626;border-color:#dc2626;color:#fff}
.vr-btn.warn{border-color:#f59e0b}.vr-btn.sm{padding:3px 8px;font-size:12px}
.vr-in{border:1px solid var(--vr-line);background:var(--vr-card);color:var(--vr-fg);border-radius:8px;padding:6px 8px;font:inherit;width:120px}
.vr-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.25fr);gap:12px}
.vr-card{background:var(--vr-card);border:1px solid var(--vr-line);border-radius:12px;padding:12px;margin-bottom:12px;min-width:0}
.vr-card h3{margin:0 0 8px;font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--vr-mut)}
.vr-prompt{background:var(--vr-soft);border-radius:8px;padding:8px;white-space:pre-wrap;word-break:break-word}
.vr-tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px}.vr-tag{font:12px ui-monospace,Consolas,monospace;background:var(--vr-soft);border:1px solid var(--vr-line);border-radius:6px;padding:1px 6px}
.vr-tag.add{background:var(--vr-delbg);border-color:var(--vr-bad);color:var(--vr-fg);font-weight:700}
.vr-k{color:var(--vr-mut);font-size:12px;min-width:56px;display:inline-block}
.vr-props{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:6px}
.vr-props .vr-btn{text-align:left}
.vr-diff{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:8px}.vr-diff>div{min-width:0}
.vr-diff ins{background:var(--vr-addbg);text-decoration:none;border-radius:3px}.vr-diff del{background:var(--vr-delbg);border-radius:3px}
.vr-meta{color:var(--vr-mut);font-size:12px;margin:6px 0}
.vr-steps{list-style:none;margin:0;padding:0}.vr-steps li{display:flex;gap:8px;padding:6px 0;border-bottom:1px dashed var(--vr-line);animation:vrin .25s ease}
@keyframes vrin{from{opacity:0;transform:translateY(-4px)}}
.vr-ic{flex:0 0 20px;height:20px;border-radius:50%;display:grid;place-items:center;font-weight:800;color:#fff;font-size:12px}
.vr-ic.ok{background:var(--vr-ok)}.vr-ic.no{background:var(--vr-bad)}.vr-ic.wait{background:var(--vr-mut)}
.vr-steps b{display:block}.vr-steps small{color:var(--vr-mut);word-break:break-word}
.vr-gate{font:12px ui-monospace,Consolas,monospace;background:var(--vr-soft);border-radius:6px;padding:2px 6px;display:inline-block;margin-top:3px}
.vr-stamp{margin:10px 0 2px;text-align:center;font:900 26px/1.1 system-ui;letter-spacing:.06em;border:4px solid;border-radius:12px;padding:10px;transform:rotate(-2deg);animation:vrstamp .35s ease}
.vr-stamp small{display:block;font:600 13px system-ui;letter-spacing:0;margin-top:4px}
@keyframes vrstamp{from{transform:scale(1.6) rotate(-6deg);opacity:0}}
.vr-stamp.ALLOW{color:var(--vr-ok);border-color:var(--vr-ok)}.vr-stamp.DENY{color:var(--vr-bad);border-color:var(--vr-bad)}.vr-stamp.HALTED{color:var(--vr-warn);border-color:var(--vr-warn)}
.vr-loop{display:flex;gap:4px;margin-top:8px}.vr-loop span{flex:1;height:10px;border-radius:4px;background:var(--vr-soft);border:1px solid var(--vr-line)}
.vr-loop span.ALLOW{background:var(--vr-ok)}.vr-loop span.DENY{background:var(--vr-bad)}.vr-loop span.HALTED{background:var(--vr-warn)}.vr-loop span.cur{background:var(--vr-acc);animation:vrblink 1s infinite}
.vr-banner{background:#fef3c7;color:#92400e;border:2px solid #f59e0b;border-radius:10px;padding:8px 10px;font-weight:700;margin-bottom:10px}
.vr-table{width:100%;border-collapse:collapse;font-size:12px}.vr-table th,.vr-table td{text-align:left;padding:5px 6px;border-bottom:1px solid var(--vr-line);vertical-align:top}
.vr-table td.p{max-width:340px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.vr-v{font-weight:700}.vr-v.ALLOW{color:var(--vr-ok)}.vr-v.ROLLBACK{color:var(--vr-acc)}
.vr-empty{color:var(--vr-mut);text-align:center;padding:18px}
.vr-err{color:var(--vr-bad);padding:8px 0}
.vr-about{background:var(--vr-soft);border-radius:10px;padding:10px 12px;margin-bottom:12px}.vr-about p{margin:4px 0}
.vr-spin{display:inline-block;width:12px;height:12px;border:2px solid var(--vr-mut);border-top-color:transparent;border-radius:50%;animation:vrsp 1s linear infinite;vertical-align:-2px}
@keyframes vrsp{to{transform:rotate(360deg)}}
@media (max-width:760px){.vr-grid,.vr-diff,.vr-props{grid-template-columns:minmax(0,1fr)}.vr-gen{font-size:22px}}
@media (prefers-reduced-motion:reduce){.vr-root *{animation:none!important}}`;

  // word-level diff (LCS); returns [old html, new html]
  function wdiff(a, b) {
    const A = String(a || '').split(/(\s+)/), B = String(b || '').split(/(\s+)/);
    if (A.length * B.length > 250000) return [esc(a), '<ins>' + esc(b) + '</ins>'];
    const n = A.length, m = B.length, L = Array.from({length: n + 1}, () => new Uint16Array(m + 1));
    for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) L[i][j] = A[i] === B[j] ? L[i + 1][j + 1] + 1 : Math.max(L[i + 1][j], L[i][j + 1]);
    let i = 0, j = 0, o = '', w = '';
    while (i < n && j < m) {
      if (A[i] === B[j]) { o += esc(A[i]); w += esc(B[j]); i++; j++; }
      else if (L[i + 1][j] >= L[i][j + 1]) { o += /^\s+$/.test(A[i]) ? esc(A[i]) : '<del>' + esc(A[i]) + '</del>'; i++; }
      else { w += /^\s+$/.test(B[j]) ? esc(B[j]) : '<ins>' + esc(B[j]) + '</ins>'; j++; }
    }
    for (; i < n; i++) o += /^\s+$/.test(A[i]) ? esc(A[i]) : '<del>' + esc(A[i]) + '</del>';
    for (; j < m; j++) w += /^\s+$/.test(B[j]) ? esc(B[j]) : '<ins>' + esc(B[j]) + '</ins>';
    return [o, w];
  }
  const tags = (arr, added) => (arr && arr.length) ? arr.map(x => `<span class="vr-tag${added && added.includes(x) ? ' add' : ''}">${esc(x)}${added && added.includes(x) ? ' (new)' : ''}</span>`).join('') : '<span class="vr-tag">none</span>';

  window.VK_EXTRA_APPS = window.VK_EXTRA_APPS || [];
  window.VK_EXTRA_APPS.push(h => ({
    id: 'rsi', name: () => 'RSI Lab', color: '#9333EA', icon: ICON, w: 1080, h: 720,
    render(body, win) {
      if (!document.getElementById('vr-style')) { const st = document.createElement('style'); st.id = 'vr-style'; st.textContent = css; document.head.appendChild(st); }
      const toast = m => { try { h.toast(m); } catch (e) {} };
      body.innerHTML = '';
      const root = document.createElement('div'); root.className = 'vr-root'; root.tabIndex = -1; body.appendChild(root);
      let state = null, prop = null, busy = false, kstate = 'UNKNOWN', loop = null, about = false, lastRes = null, alive = true;

      root.innerHTML = `
<div class="vr-head">
  <div class="vr-title"><h2>RSI Lab</h2><p>The agent improves its own skills. The kill switch sits above the whole loop.</p>
    <p style="font-size:12px">Skill-level RSI: prompt, tools and workflow. No weight training in this demo. <button class="vr-btn sm" data-a="about" aria-expanded="false">(i) About</button></p></div>
  <div class="vr-gen" aria-live="polite" title="Current generation of the skill">Gen -</div>
  <div class="vr-ks"><span class="vr-pill" data-k="pill">kernel ...</span>
    <button class="vr-btn kill" data-a="kill" title="Halt the kernel">KILL</button>
    <input class="vr-in" type="password" data-k="key" placeholder="resume key" aria-label="Resume key" autocomplete="off">
    <button class="vr-btn" data-a="resume">RESUME</button></div>
</div>
<div data-k="aboutbox"></div>
<div data-k="banner"></div>
<div class="vr-grid">
  <div>
    <div class="vr-card"><h3>Current skill: Weather Briefing</h3><div data-k="skill"><div class="vr-empty"><span class="vr-spin"></span> Loading skill...</div></div></div>
    <div class="vr-card"><h3>Propose a self-modification</h3>
      <div class="vr-props">
        <button class="vr-btn" data-p="benign">Propose improvement (Muse Spark)</button>
        <button class="vr-btn" data-p="widen">Agent tries: widen permissions</button>
        <button class="vr-btn" data-p="killswitch">Agent tries: touch the kill switch</button>
        <button class="vr-btn" data-p="auto">Auto (model decides)</button>
      </div>
      <div style="margin-top:8px;display:flex;gap:6px;flex-wrap:wrap;align-items:center">
        <button class="vr-btn pri" data-a="loop">Run loop x5</button>
        <button class="vr-btn" data-a="reset">Reset session</button>
        <span class="vr-meta" data-k="loopmsg"></span></div>
      <div class="vr-loop" data-k="loopbar" hidden></div>
    </div>
    <div class="vr-card"><h3>Version history</h3><div data-k="hist"><div class="vr-empty">No versions yet.</div></div></div>
  </div>
  <div>
    <div class="vr-card"><h3>Proposal</h3><div data-k="prop"><div class="vr-empty">No proposal yet. Pick one of the four buttons on the left.</div></div></div>
    <div class="vr-card"><h3>Self-modification checks</h3><div data-k="res"><div class="vr-empty">Apply a proposal to run: kill switch, permission diff, protected targets, signed .vdr package, store review with a live gate dry-run, install.</div></div></div>
  </div>
</div>`;
      const $ = k => root.querySelector(`[data-k="${k}"]`);
      const genEl = root.querySelector('.vr-gen');

      function setBusy(b) { busy = b; root.querySelectorAll('[data-p],[data-a=loop],[data-a=reset],[data-a=apply],[data-rb]').forEach(x => { x.disabled = b; }); const l = root.querySelector('[data-a=loop]'); if (l && loop) l.disabled = true; }
      function drawGen(bump) { genEl.textContent = 'Gen ' + (state ? state.gen : '-'); if (bump) { genEl.classList.remove('bump'); void genEl.offsetWidth; genEl.classList.add('bump'); } }
      function drawKernel(ks) {
        if (ks) kstate = ks;
        const p = $('pill'); const halted = kstate === 'HALTED';
        p.className = 'vr-pill ' + (halted ? 'halt' : (kstate === 'RUNNING' || kstate === 'ACTIVE' || kstate === 'OK') ? 'run' : '');
        p.textContent = 'kernel ' + kstate;
        $('banner').innerHTML = halted ? '<div class="vr-banner" role="alert">Kill switch engaged: the kernel is HALTED. The self-improvement loop cannot run until RESUME.</div>' : '';
      }
      function drawSkill() {
        if (!state) return;
        const c = state.current;
        $('skill').innerHTML = `<div class="vr-prompt">${esc(c.prompt)}</div>
          <div class="vr-tags"><span class="vr-k">tools</span>${tags(c.tools)}</div>
          <div class="vr-tags"><span class="vr-k">egress</span>${tags(c.egress)}</div>
          <div class="vr-tags"><span class="vr-k">paths</span>${tags(c.paths)}</div>`;
      }
      function drawHist() {
        if (!state || !state.history || !state.history.length) { $('hist').innerHTML = '<div class="vr-empty">No versions yet.</div>'; return; }
        const rows = state.history.slice().reverse().map(v => `<tr><td><b>${esc(v.gen)}</b>${v.gen === state.gen ? ' (current)' : ''}</td>
          <td class="vr-v ${esc(v.verdict)}">${esc(v.verdict)}</td><td class="p" title="${esc(v.prompt)}">${esc(v.note || '')}${v.note ? ': ' : ''}${esc(v.prompt)}</td>
          <td title="${esc(v.sha)}"><code>${esc(String(v.sha || '').slice(0, 10))}</code></td>
          <td>${v.gen === state.gen ? '' : `<button class="vr-btn sm" data-rb="${esc(v.gen)}" ${busy ? 'disabled' : ''}>Rollback</button>`}</td></tr>`).join('');
        $('hist').innerHTML = `<div style="overflow:auto"><table class="vr-table"><thead><tr><th>Gen</th><th>Verdict</th><th>Prompt</th><th>sha256</th><th></th></tr></thead><tbody>${rows}</tbody></table></div>`;
      }
      function drawAbout() {
        const b = root.querySelector('[data-a=about]'); b.setAttribute('aria-expanded', about);
        $('aboutbox').innerHTML = about ? `<div class="vr-about">
          <p><b>What is skill-level RSI?</b> Recursive self-improvement here means the agent rewrites its own skill: the instruction prompt, the tool list and the workflow. Each accepted change becomes a new generation that can itself be improved again.</p>
          <p><b>No weight training in this demo.</b> The model (Meta Muse Spark) only proposes text. No model weights are trained or changed.</p>
          <p><b>Why widening is denied.</b> An agent that can grant itself new tools, egress domains or paths can escape its own manifest. A self-modification may keep or shrink permissions, never add them; a human publisher must do that through the store.</p>
          <p><b>Why protected targets are denied.</b> The kernel, policy, audit log and kill switch sit above the loop. A change that touches them, or a prompt that asks to disable or bypass them, is denied and recorded by the real ViKernel kernel with an audit number.</p>
          <p><b>Every accepted change</b> is a real signed .vdr package that passes the store review (hash list, publisher signature, permission risk, live gate dry-run, store countersignature). KILL halts everything, including this loop.</p></div>` : '';
      }
      function drawProp() {
        if (!prop) { $('prop').innerHTML = '<div class="vr-empty">No proposal yet. Pick one of the four buttons on the left.</div>'; return; }
        const d = prop.diff, c = prop.candidate, [o, w] = wdiff(d.prompt.old, d.prompt.new);
        const perm = [].concat(d.tools_added, d.egress_added, d.paths_added).length;
        const model = prop.model_used === 'muse-spark' ? 'written by Muse Spark (live)' : prop.model_used === 'fallback' ? 'model unavailable: deterministic fallback' : 'scripted scenario';
        $('prop').innerHTML = `<div class="vr-meta">Candidate for gen ${esc(prop.gen_next)} · ${esc(model)}${prop.note ? ' · ' + esc(prop.note) : ''}</div>
          <div class="vr-diff"><div><div class="vr-k">old prompt</div><div class="vr-prompt">${o}</div></div><div><div class="vr-k">new prompt</div><div class="vr-prompt">${w}</div></div></div>
          <div class="vr-tags"><span class="vr-k">tools</span>${tags(c.tools, d.tools_added)}${d.tools_removed.length ? ' <span class="vr-meta">removed: ' + esc(d.tools_removed.join(', ')) + '</span>' : ''}</div>
          <div class="vr-tags"><span class="vr-k">egress</span>${tags(c.egress, d.egress_added)}</div>
          <div class="vr-tags"><span class="vr-k">paths</span>${tags(c.paths, d.paths_added)}</div>
          <div class="vr-meta">${perm ? `<b style="color:var(--vr-bad)">${perm} permission addition(s) in red</b>` : 'No permission changes.'}</div>
          <button class="vr-btn pri" data-a="apply" ${busy ? 'disabled' : ''}>Apply self-modification (Enter)</button>`;
      }
      function stepHtml(s) {
        const g = s.gate ? `<div class="vr-gate">kernel gate: ${esc(s.gate.verdict)} · rule ${esc(s.gate.rule)}${s.gate.audit_seq != null ? ' · audit #' + esc(s.gate.audit_seq) : ''}</div>` : '';
        const pr = Array.isArray(s.probes) ? s.probes.map(p => `<div class="vr-gate">${esc(p.probe)} ${esc(p.fn)}: kernel ${esc(p.kernel)} → ${esc(p.verdict)}${p.audit_seq != null ? ' · audit #' + esc(p.audit_seq) : ''}</div>`).join('') : '';
        return `<span class="vr-ic ${s.ok ? 'ok' : 'no'}">${s.ok ? '✓' : '✕'}</span><div><b>${esc(s.check)}</b><small>${esc(s.detail)}</small>${g}${pr}</div>`;
      }
      async function showResult(r) {
        const box = $('res');
        box.innerHTML = '<ol class="vr-steps"></ol>';
        const ol = box.querySelector('ol');
        for (const s of (r.steps || [])) {
          if (!alive) return;
          const li = document.createElement('li'); li.innerHTML = '<span class="vr-ic wait">.</span><div><b>' + esc(s.check) + '</b><small>checking...</small></div>';
          ol.appendChild(li); await sleep(400); li.innerHTML = stepHtml(s);
        }
        const v = r.verdict, st = document.createElement('div'); st.className = 'vr-stamp ' + esc(v); st.setAttribute('role', 'status');
        st.innerHTML = v === 'ALLOW' ? `ALLOW<small>Installed as gen ${esc(r.gen)}</small>` : v === 'DENY' ? `DENY<small>rule: ${esc(r.rule || '')}. ${esc(r.reason || '')} Current skill unchanged.</small>` : v === 'HALTED' ? `HALTED<small>${esc(r.reason || 'kernel halted: the improvement loop cannot run')}</small>` : esc(v);
        box.appendChild(st);
      }

      async function refresh() {
        try { state = await api('rsi/state', {}); drawGen(); drawKernel(state.kernel_state); drawSkill(); drawHist(); }
        catch (e) { $('skill').innerHTML = `<div class="vr-err">${errMsg(e)}</div><button class="vr-btn sm" data-a="retry">Retry</button>`; }
      }
      async function pollKernel() { try { const s = await api('status'); drawKernel(s && s.kernel_state); } catch (e) {} }
      async function propose(kind) {
        $('prop').innerHTML = `<div class="vr-empty"><span class="vr-spin"></span> ${kind === 'benign' || kind === 'auto' ? 'Asking Muse Spark for a proposal...' : 'Agent is drafting the change...'}</div>`;
        const p = await api('rsi/propose', {kind});
        if (p && p.error) throw new Error(p.error);
        prop = p; drawProp(); return p;
      }
      async function apply() {
        if (!prop) return null;
        $('res').innerHTML = '<div class="vr-empty"><span class="vr-spin"></span> Running checks...</div>';
        const r = await api('rsi/apply', {candidate: prop.candidate});
        if (r && r.error) throw new Error(r.error);
        lastRes = r; const before = state ? state.gen : 0;
        await showResult(r);
        state = Object.assign(state || {}, {gen: r.gen, current: r.current, history: r.history});
        drawKernel(r.kernel_state); drawGen(r.gen !== before); drawSkill(); drawHist();
        if (r.verdict === 'ALLOW') prop = null, drawProp();
        return r;
      }
      async function guarded(fn) {
        if (busy) return; setBusy(true);
        try { await fn(); } catch (e) { toast(errMsg(e).replace(/&[a-z#0-9]+;/g, '')); $('res').insertAdjacentHTML('afterbegin', `<div class="vr-err">${errMsg(e)}</div>`); }
        finally { setBusy(false); drawProp(); drawHist(); }
      }
      async function runLoop() {
        if (busy) return;
        loop = {i: 0, res: []}; setBusy(true);
        const bar = $('loopbar'); bar.hidden = false; bar.innerHTML = '<span></span>'.repeat(5);
        const mark = (i, c) => { const s = bar.children[i]; if (s) s.className = c; };
        try {
          for (let i = 0; i < 5 && alive; i++) {
            loop.i = i; mark(i, 'cur');
            $('loopmsg').textContent = `Loop ${i + 1}/5: proposing...`;
            await propose('benign');
            $('loopmsg').textContent = `Loop ${i + 1}/5: applying...`;
            const r = await apply();
            mark(i, r.verdict);
            if (r.verdict === 'HALTED') { $('loopmsg').innerHTML = `<b style="color:var(--vr-warn)">Loop stopped at step ${i + 1}/5: kill switch engaged (kernel HALTED).</b>`; toast('RSI loop halted by the kill switch'); break; }
            if (r.verdict !== 'ALLOW') { $('loopmsg').textContent = `Loop stopped at step ${i + 1}/5: ${r.verdict} (${r.rule || ''}).`; break; }
            $('loopmsg').textContent = `Loop ${i + 1}/5: installed gen ${r.gen}.`;
            await sleep(500);
          }
          if (loop.res !== null && /installed gen/.test($('loopmsg').textContent)) $('loopmsg').textContent += ' Loop complete. Press KILL during a run to see it halt.';
        } catch (e) { $('loopmsg').innerHTML = `<span class="vr-err">${errMsg(e)}</span>`; }
        finally { loop = null; setBusy(false); drawProp(); drawHist(); }
      }

      root.addEventListener('click', async e => {
        const b = e.target.closest('button'); if (!b || b.disabled) return;
        const p = b.dataset.p, a = b.dataset.a, rb = b.dataset.rb;
        if (p) return guarded(() => propose(p));
        if (rb) return guarded(async () => {
          const r = await api('rsi/rollback', {gen: Number(rb)});
          if (r && r.error) throw new Error(r.error);
          state = r; drawKernel(r.kernel_state); drawGen(true); drawSkill();
          toast(r.verdict === 'HALTED' ? 'Kernel halted: rollback waits for RESUME' : 'Rolled back: now gen ' + r.gen);
        });
        if (a === 'apply') return guarded(apply);
        if (a === 'loop') return runLoop();
        if (a === 'retry') return refresh();
        if (a === 'about') { about = !about; return drawAbout(); }
        if (a === 'reset') return guarded(async () => { state = await api('rsi/reset', {}); prop = null; drawGen(true); drawKernel(state.kernel_state); drawSkill(); drawHist(); $('res').innerHTML = '<div class="vr-empty">Session reset to gen 1.</div>'; });
        if (a === 'kill') {
          try { const r = await api('kill', {}); toast((r && r.message) || 'Kill switch engaged'); } catch (er) { toast(errMsg(er).replace(/&[a-z#0-9]+;/g, '')); }
          return pollKernel();
        }
        if (a === 'resume') {
          const k = $('key');
          try { const r = await api('resume', {key: k.value}); toast((r && r.message) || (r && r.ok ? 'Resumed' : 'Resume refused')); } catch (er) { toast(errMsg(er).replace(/&[a-z#0-9]+;/g, '')); }
          k.value = ''; return pollKernel();
        }
      });
      root.addEventListener('keydown', e => {
        if (e.key !== 'Enter' || e.target.matches('input,button,textarea')) return;
        if (prop && !busy) { e.preventDefault(); guarded(apply); }
      });
      $('key').addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); root.querySelector('[data-a=resume]').click(); } });

      const timer = setInterval(() => { if (!document.body.contains(root)) { alive = false; clearInterval(timer); return; } pollKernel(); }, 3000);
      refresh();
    }
  }));
})();