mindforge-cc 11.9.4 → 11.9.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.
@@ -33,21 +33,33 @@
33
33
 
34
34
  // ── State ─────────────────────────────────────────────────────────────────
35
35
  let currentApprovals = [];
36
- let state = { costs: [], quality: [] };
36
+ let state = { sessions: [] }; // replaced wholesale by refreshData() with the /api/metrics response
37
37
 
38
38
  function showPage(id) {
39
- document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
40
- document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
39
+ document
40
+ .querySelectorAll('.page')
41
+ .forEach((p) => p.classList.remove('active'));
42
+ document
43
+ .querySelectorAll('.tab')
44
+ .forEach((t) => t.classList.remove('active'));
41
45
  document.getElementById(id).classList.add('active');
42
- const tab = Array.from(document.querySelectorAll('.tab')).find(t => t.innerText.toLowerCase() === id.toLowerCase());
46
+ const tab = Array.from(document.querySelectorAll('.tab')).find(
47
+ (t) => t.innerText.toLowerCase() === id.toLowerCase(),
48
+ );
43
49
  if (tab) tab.classList.add('active');
44
-
50
+
45
51
  if (id === 'metrics') drawCharts();
46
52
  }
47
53
 
48
54
  function escapeHtml(str) {
49
55
  if (!str) return '';
50
- return String(str).replace(/[&<>"']/g, m => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', '\'':'&#39;' }[m]));
56
+ return String(str).replace(
57
+ /[&<>"']/g,
58
+ (m) =>
59
+ ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', '\'': '&#39;' })[
60
+ m
61
+ ],
62
+ );
51
63
  }
52
64
 
53
65
  // ── WebSocket / SSE ───────────────────────────────────────────────────────
@@ -95,9 +107,11 @@ async function refreshData() {
95
107
  refreshTeam();
96
108
  refreshRevOps();
97
109
  if (document.getElementById('temporal').classList.contains('active')) {
98
- refreshTemporal();
110
+ refreshTemporal();
99
111
  }
100
- } catch(e) { console.error('Refresh fail', e); }
112
+ } catch (e) {
113
+ console.error('Refresh fail', e);
114
+ }
101
115
  }
102
116
 
103
117
  async function refreshRevOps() {
@@ -105,9 +119,11 @@ async function refreshRevOps() {
105
119
  const res = await fetch('/api/revops/overview');
106
120
  const data = await res.json();
107
121
  if (data.success) {
108
- updateRevOpsUI(data);
122
+ updateRevOpsUI(data);
109
123
  }
110
- } catch { /* one panel failing must not abort the rest of the refresh; it keeps its last value */ }
124
+ } catch {
125
+ /* one panel failing must not abort the rest of the refresh; it keeps its last value */
126
+ }
111
127
  }
112
128
 
113
129
  let temporalHistory = [];
@@ -117,7 +133,9 @@ async function refreshTemporal() {
117
133
  const data = await res.json();
118
134
  temporalHistory = data;
119
135
  renderTemporalTimeline();
120
- } catch { /* one panel failing must not abort the rest of the refresh; it keeps its last value */ }
136
+ } catch {
137
+ /* one panel failing must not abort the rest of the refresh; it keeps its last value */
138
+ }
121
139
  }
122
140
 
123
141
  async function refreshApprovals() {
@@ -126,16 +144,25 @@ async function refreshApprovals() {
126
144
  const data = await res.json();
127
145
  currentApprovals = data;
128
146
  renderApprovals();
129
- } catch { /* one panel failing must not abort the rest of the refresh; it keeps its last value */ }
147
+ } catch {
148
+ /* one panel failing must not abort the rest of the refresh; it keeps its last value */
149
+ }
130
150
  }
131
151
 
132
152
  async function refreshMemory() {
133
153
  try {
134
154
  const res = await fetch('/api/memory');
155
+ // Metrics.getMemory() returns { entries, total } — a flat, confidence-sorted knowledge-base
156
+ // list, not a graph. This panel used to read `data.graph`/`data.count`, fields that endpoint
157
+ // has never produced, so it always rendered "{}" and "0 Items Indexed".
135
158
  const data = await res.json();
136
- document.getElementById('memory-map').innerHTML = `<pre>${escapeHtml(JSON.stringify(data.graph || {}, null, 2))}</pre>`;
137
- document.getElementById('memory-stats-list').innerHTML = `<div class="stat-value">${data.count || 0} <span class="stat-unit">Items Indexed</span></div>`;
138
- } catch { /* one panel failing must not abort the rest of the refresh; it keeps its last value */ }
159
+ document.getElementById('memory-map').innerHTML =
160
+ `<pre>${escapeHtml(JSON.stringify(data.entries || [], null, 2))}</pre>`;
161
+ document.getElementById('memory-stats-list').innerHTML =
162
+ `<div class="stat-value">${data.total || 0} <span class="stat-unit">Items Indexed</span></div>`;
163
+ } catch {
164
+ /* one panel failing must not abort the rest of the refresh; it keeps its last value */
165
+ }
139
166
  }
140
167
 
141
168
  async function refreshTeam() {
@@ -143,7 +170,9 @@ async function refreshTeam() {
143
170
  const res = await fetch('/api/team');
144
171
  const data = await res.json();
145
172
  renderTeam(data);
146
- } catch { /* one panel failing must not abort the rest of the refresh; it keeps its last value */ }
173
+ } catch {
174
+ /* one panel failing must not abort the rest of the refresh; it keeps its last value */
175
+ }
147
176
  }
148
177
 
149
178
  // decide() and submitDecision() are removed along with POST /api/approve/:id. They sent
@@ -153,25 +182,32 @@ async function refreshTeam() {
153
182
  // by bin/governance/approve.js, which takes identity from git and fails closed without a GPG
154
183
  // key unless MINDFORGE_ALLOW_UNVERIFIED_APPROVAL=1.
155
184
 
156
-
157
185
  function closeConfirm() {
158
186
  document.getElementById('confirm-overlay').style.display = 'none';
159
187
  }
160
188
 
161
189
  // ── UI Rendering ──────────────────────────────────────────────────────────
162
190
  function updateStatusUI(data) {
163
- document.getElementById('project-name').textContent = `PROJ: ${data.project_name || 'UNSET'}`;
191
+ document.getElementById('project-name').textContent =
192
+ `PROJ: ${data.project_name || 'UNSET'}`;
164
193
  document.getElementById('stat-phase').textContent = data.phase || '0';
165
- document.getElementById('stat-status').textContent = (data.auto_status || 'IDLE').toUpperCase();
166
- document.getElementById('stat-tasks').textContent = `${data.tasks_completed || 0}/${data.tasks_total || 0}`;
167
- document.getElementById('stat-elapsed').textContent = data.elapsed_ms ? (data.elapsed_ms / 1000).toFixed(1) + 's' : '0s';
194
+ document.getElementById('stat-status').textContent = (
195
+ data.auto_status || 'IDLE'
196
+ ).toUpperCase();
197
+ document.getElementById('stat-tasks').textContent =
198
+ `${data.tasks_completed || 0}/${data.tasks_total || 0}`;
199
+ document.getElementById('stat-elapsed').textContent = data.elapsed_ms
200
+ ? (data.elapsed_ms / 1000).toFixed(1) + 's'
201
+ : '0s';
168
202
  }
169
203
 
170
204
  function updateMetricsUI(data) {
171
205
  // /api/metrics returns { sessions, avg_quality, avg_cost_usd, ... } — see
172
206
  // bin/dashboard/metrics-aggregator.js getMetrics(). It has never returned
173
207
  // `costs` or `quality`, so these tiles used to render "$0.0000" and "NaN".
174
- document.getElementById('stat-avg-quality').textContent = (data.avg_quality ?? 0).toFixed(1);
208
+ document.getElementById('stat-avg-quality').textContent = (
209
+ data.avg_quality ?? 0
210
+ ).toFixed(1);
175
211
  }
176
212
 
177
213
  // Cumulative spend comes from the usage ledger via /api/costs (already wired
@@ -184,14 +220,22 @@ async function refreshCosts() {
184
220
  // and genuine zero spend look identical under the "Cost (7d)" label.
185
221
  if (!res.ok) throw new Error(`/api/costs ${res.status}`);
186
222
  const data = await res.json();
187
- document.getElementById('stat-total-cost').textContent = `${(data.total_usd ?? 0).toFixed(2)}`;
188
- } catch (e) { console.error('Cost refresh failed', e); }
223
+ document.getElementById('stat-total-cost').textContent =
224
+ `${(data.total_usd ?? 0).toFixed(2)}`;
225
+ } catch (e) {
226
+ console.error('Cost refresh failed', e);
227
+ }
189
228
  }
190
229
 
191
230
  function appendAuditEvent(data) {
192
231
  const div = document.createElement('div');
193
232
  div.className = `event type-${data.event}`;
194
- const time = new Date(data.timestamp).toLocaleTimeString([], { hour12:false, hour:'2-digit', minute:'2-digit', second:'2-digit' });
233
+ const time = new Date(data.timestamp).toLocaleTimeString([], {
234
+ hour12: false,
235
+ hour: '2-digit',
236
+ minute: '2-digit',
237
+ second: '2-digit',
238
+ });
195
239
  div.innerHTML = `<span class="event-time">${time}</span><span class="event-type type-${data.event}">${data.event}</span><span class="event-msg">${escapeHtml(data.message || data.task || '')}</span>`;
196
240
  feed.prepend(div);
197
241
  if (feed.children.length > 200) feed.lastChild.remove();
@@ -208,22 +252,33 @@ function renderApprovals() {
208
252
  const list = document.getElementById('approval-list');
209
253
  const records = Array.isArray(currentApprovals) ? currentApprovals : [];
210
254
  if (records.length === 0) {
211
- list.innerHTML = '<div style="text-align:center; padding: 40px; color:var(--muted)">'
212
- + 'No approval records. Tier-3 review is a recorded human acknowledgement; '
213
- + 'authorization is enforced by branch protection, not from here.</div>';
255
+ list.innerHTML =
256
+ '<div style="text-align:center; padding: 40px; color:var(--muted)">' +
257
+ 'No approval records. Tier-3 review is a recorded human acknowledgement; ' +
258
+ 'authorization is enforced by branch protection, not from here.</div>';
214
259
  return;
215
260
  }
216
261
 
217
- const colour = { valid: 'var(--green)', stale: 'var(--purple)', corrupt: 'var(--red)' };
218
- list.innerHTML = records.map(a => {
219
- const hrs = a.hours_remaining;
220
- const life = (hrs === null || hrs === undefined) ? ''
221
- : hrs > 0 ? `${hrs.toFixed(1)}h remaining` : `expired ${Math.abs(hrs).toFixed(1)}h ago`;
222
- const problems = (a.problems || []).length
223
- ? '<ul style="margin:8px 0 0 16px; font-size:11px; color:var(--red)">'
224
- + a.problems.map(x => `<li>${escapeHtml(x)}</li>`).join('') + '</ul>'
225
- : '';
226
- return `
262
+ const colour = {
263
+ valid: 'var(--green)',
264
+ stale: 'var(--purple)',
265
+ corrupt: 'var(--red)',
266
+ };
267
+ list.innerHTML = records
268
+ .map((a) => {
269
+ const hrs = a.hours_remaining;
270
+ const life =
271
+ hrs === null || hrs === undefined
272
+ ? ''
273
+ : hrs > 0
274
+ ? `${hrs.toFixed(1)}h remaining`
275
+ : `expired ${Math.abs(hrs).toFixed(1)}h ago`;
276
+ const problems = (a.problems || []).length
277
+ ? '<ul style="margin:8px 0 0 16px; font-size:11px; color:var(--red)">' +
278
+ a.problems.map((x) => `<li>${escapeHtml(x)}</li>`).join('') +
279
+ '</ul>'
280
+ : '';
281
+ return `
227
282
  <div class="card approval-card">
228
283
  <div class="card-body">
229
284
  <div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:12px;">
@@ -241,33 +296,64 @@ function renderApprovals() {
241
296
  ${problems}
242
297
  </div>
243
298
  </div>`;
244
- }).join('');
299
+ })
300
+ .join('');
245
301
  }
246
302
 
247
303
  function renderTeam(data) {
304
+ // Metrics.getTeamActivity() returns { active, conflicts } (not a flat array), and each
305
+ // `active` entry has email/last_seen/current_task/event fields, not user/action/timestamp.
306
+ // Reading `data.length`/`data.map()` on that object never rendered anything: `.length` is
307
+ // undefined so the empty-state guard below never fired, and `.map()` on a plain object threw,
308
+ // silently swallowed by refreshTeam()'s catch.
248
309
  const list = document.getElementById('team-activity-list');
249
- if (!data || data.length === 0) {
250
- list.innerHTML = '<div style="text-align:center; padding: 40px; color:var(--muted)">No recent team activity</div>';
310
+ const active = Array.isArray(data?.active) ? data.active : [];
311
+ if (active.length === 0) {
312
+ list.innerHTML =
313
+ '<div style="text-align:center; padding: 40px; color:var(--muted)">No recent team activity</div>';
251
314
  return;
252
315
  }
253
- list.innerHTML = data.map(ev => `
316
+ list.innerHTML = active
317
+ .map(
318
+ (ev) => `
254
319
  <div class="activity-row">
255
- <div class="avatar">${(ev.user || '?').charAt(0).toUpperCase()}</div>
320
+ <div class="avatar">${(ev.email || '?').charAt(0).toUpperCase()}</div>
256
321
  <div class="activity-info">
257
- <div><span class="activity-user">${escapeHtml(ev.user)}</span> <span class="activity-action">${escapeHtml(ev.action)}</span></div>
258
- <div class="activity-time">${new Date(ev.timestamp).toLocaleString()}</div>
322
+ <div><span class="activity-user">${escapeHtml(ev.email)}</span> <span class="activity-action">${escapeHtml(ev.current_task || ev.event || '')}</span></div>
323
+ <div class="activity-time">${new Date(ev.last_seen).toLocaleString()}</div>
259
324
  </div>
260
325
  </div>
261
- `).join('');
326
+ `,
327
+ )
328
+ .join('');
262
329
  }
263
330
 
264
331
  // ── Simple Charts Implementation ──────────────────────────────────────────
265
332
  function drawCharts() {
266
- drawCanvasChart('chart-costs', state.costs?.map(c => c.cost) || [], varColor('--accent'));
267
- drawCanvasChart('chart-quality', state.quality?.map(q => q.score) || [], varColor('--green'), 100);
333
+ // refreshData() does `state = data` with the raw /api/metrics response, which is
334
+ // { sessions, avg_quality, avg_cost_usd, ... } — it has never had `costs`/`quality`
335
+ // properties, so state.costs/state.quality were always undefined past the first load
336
+ // and these charts rendered permanently empty. The real per-session series lives at
337
+ // state.sessions[].cost_usd / state.sessions[].quality_score.
338
+ const sessions = state.sessions || [];
339
+ drawCanvasChart(
340
+ 'chart-costs',
341
+ sessions.map((s) => s.cost_usd),
342
+ varColor('--accent'),
343
+ );
344
+ drawCanvasChart(
345
+ 'chart-quality',
346
+ sessions.map((s) => s.quality_score),
347
+ varColor('--green'),
348
+ 100,
349
+ );
268
350
  }
269
351
 
270
- function varColor(name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); }
352
+ function varColor(name) {
353
+ return getComputedStyle(document.documentElement)
354
+ .getPropertyValue(name)
355
+ .trim();
356
+ }
271
357
 
272
358
  function drawCanvasChart(id, data, color, maxVal = null) {
273
359
  const canvas = document.getElementById(id);
@@ -280,27 +366,30 @@ function drawCanvasChart(id, data, color, maxVal = null) {
280
366
 
281
367
  const w = canvas.offsetWidth;
282
368
  const h = canvas.offsetHeight;
283
- ctx.clearRect(0,0,w,h);
284
-
369
+ ctx.clearRect(0, 0, w, h);
370
+
285
371
  if (data.length < 2) return;
286
-
372
+
287
373
  const max = maxVal || Math.max(...data) * 1.2 || 1;
288
374
  const step = w / (data.length - 1);
289
-
375
+
290
376
  ctx.beginPath();
291
377
  ctx.strokeStyle = color;
292
378
  ctx.lineWidth = 2;
293
379
  ctx.lineJoin = 'round';
294
-
380
+
295
381
  data.forEach((val, i) => {
296
382
  const x = i * step;
297
383
  const y = h - (val / max) * h;
298
- if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
384
+ if (i === 0) ctx.moveTo(x, y);
385
+ else ctx.lineTo(x, y);
299
386
  });
300
387
  ctx.stroke();
301
388
 
302
389
  // Area fill
303
- ctx.lineTo(w, h); ctx.lineTo(0, h); ctx.closePath();
390
+ ctx.lineTo(w, h);
391
+ ctx.lineTo(0, h);
392
+ ctx.closePath();
304
393
  ctx.fillStyle = color.replace(')', ', 0.1)').replace('rgb', 'rgba');
305
394
  ctx.fill();
306
395
  }
@@ -309,7 +398,7 @@ function drawCanvasChart(id, data, color, maxVal = null) {
309
398
  function renderTemporalTimeline() {
310
399
  const slider = document.getElementById('temporal-slider');
311
400
  const count = document.getElementById('timeline-count');
312
-
401
+
313
402
  if (temporalHistory.length === 0) {
314
403
  slider.max = 0;
315
404
  count.textContent = '0 Snapshots';
@@ -318,11 +407,11 @@ function renderTemporalTimeline() {
318
407
 
319
408
  slider.max = temporalHistory.length - 1;
320
409
  count.textContent = `${temporalHistory.length} Snapshots`;
321
-
410
+
322
411
  // Default to latest if not touching
323
412
  if (slider.value == 0 && temporalHistory.length > 0) {
324
- onSliderChange(temporalHistory.length - 1);
325
- slider.value = temporalHistory.length - 1;
413
+ onSliderChange(temporalHistory.length - 1);
414
+ slider.value = temporalHistory.length - 1;
326
415
  }
327
416
  }
328
417
 
@@ -332,16 +421,19 @@ async function onSliderChange(index) {
332
421
  if (!snap) return;
333
422
 
334
423
  selectedSnapshot = snap;
335
- document.getElementById('slider-current').textContent = `Point: ${snap.id.slice(0, 8)} (${new Date(snap.timestamp).toLocaleTimeString()})`;
424
+ document.getElementById('slider-current').textContent =
425
+ `Point: ${snap.id.slice(0, 8)} (${new Date(snap.timestamp).toLocaleTimeString()})`;
336
426
  document.getElementById('inject-btn').disabled = false;
337
427
 
338
428
  // Fetch sample audit file for this snapshot
339
429
  try {
340
430
  const res = await fetch(`/api/temporal/snapshot/${snap.id}/AUDIT.jsonl`);
341
431
  const content = await res.text();
342
- document.getElementById('snapshot-viewer').textContent = content || 'No audit log available for this point.';
343
- } catch(e) {
344
- document.getElementById('snapshot-viewer').textContent = 'Failed to load snapshot details.';
432
+ document.getElementById('snapshot-viewer').textContent =
433
+ content || 'No audit log available for this point.';
434
+ } catch (e) {
435
+ document.getElementById('snapshot-viewer').textContent =
436
+ 'Failed to load snapshot details.';
345
437
  }
346
438
  }
347
439
 
@@ -349,8 +441,8 @@ async function injectHindsight() {
349
441
  if (!selectedSnapshot) return;
350
442
  const instruction = document.getElementById('steering-input').value;
351
443
  if (!instruction) {
352
- alert('Please provide a steering instruction for the hindsight injection.');
353
- return;
444
+ alert('Please provide a steering instruction for the hindsight injection.');
445
+ return;
354
446
  }
355
447
 
356
448
  const btn = document.getElementById('inject-btn');
@@ -363,50 +455,59 @@ async function injectHindsight() {
363
455
  headers: { 'Content-Type': 'application/json' },
364
456
  body: JSON.stringify({
365
457
  auditId: selectedSnapshot.id,
366
- fixDescription: instruction
367
- })
458
+ fixDescription: instruction,
459
+ }),
368
460
  });
369
461
  const result = await res.json();
370
462
  if (result.success) {
371
- alert('Hindsight Injection Successful. Agent state rolled back and awaiting re-optimization.');
372
- showPage('activity');
463
+ alert(
464
+ 'Hindsight Injection Successful. Agent state rolled back and awaiting re-optimization.',
465
+ );
466
+ showPage('activity');
373
467
  } else {
374
- alert('Injection failed: ' + result.error);
468
+ alert('Injection failed: ' + result.error);
375
469
  }
376
- } catch(e) {
377
- alert('Failed to connect to temporal engine.');
470
+ } catch (e) {
471
+ alert('Failed to connect to temporal engine.');
378
472
  } finally {
379
- btn.disabled = false;
380
- btn.textContent = 'Rewind & Inject Fix';
473
+ btn.disabled = false;
474
+ btn.textContent = 'Rewind & Inject Fix';
381
475
  }
382
476
  }
383
477
 
384
478
  // ── RevOps Logic ─────────────────────────────────────────────────────────
385
479
  function updateRevOpsUI(data) {
386
- const { roi, velocity, debt } = data;
387
-
388
- document.getElementById('rev-roi').textContent = `${roi.roi_percentage}%`;
389
- document.getElementById('rev-net').textContent = `$${roi.net_value}`;
390
- document.getElementById('rev-velocity').innerHTML = `${velocity.avg_seconds_per_task} <span class="stat-unit">sec/task</span>`;
391
- document.getElementById('rev-eta').textContent = `ETA: ${velocity.eta}`;
392
- document.getElementById('rev-health').textContent = debt.security_health_score;
393
-
394
- const healthStatus = document.getElementById('rev-health-status');
395
- healthStatus.textContent = `STATUS: ${debt.governance_status.toUpperCase()}`;
396
- healthStatus.style.background = debt.security_health_score > 80 ? 'rgba(63, 185, 80, 0.15)' : 'rgba(248, 81, 73, 0.15)';
397
- healthStatus.style.color = debt.security_health_score > 80 ? 'var(--green)' : 'var(--red)';
398
-
399
- document.getElementById('roi-hours').textContent = `${roi.hours_saved}h`;
400
- document.getElementById('roi-gross').textContent = `$${roi.gross_value}`;
401
- document.getElementById('roi-burn').textContent = `$${roi.token_cost}`;
402
- document.getElementById('roi-total-pct').textContent = `${roi.roi_percentage}%`;
403
-
404
- document.getElementById('debt-critical').textContent = debt.critical_findings;
405
- document.getElementById('debt-tier3').textContent = debt.tier3_approvals;
406
- const riskBadge = document.getElementById('debt-risk');
407
- riskBadge.textContent = debt.debt_level.toUpperCase();
408
- riskBadge.className = `badge ${debt.debt_level === 'Minimal' ? 'badge-live' : ''}`;
409
- if (debt.debt_level !== 'Minimal') riskBadge.style.color = 'var(--yellow)';
480
+ const { roi, velocity, debt } = data;
481
+
482
+ document.getElementById('rev-roi').textContent = `${roi.roi_percentage}%`;
483
+ document.getElementById('rev-net').textContent = `$${roi.net_value}`;
484
+ document.getElementById('rev-velocity').innerHTML =
485
+ `${velocity.avg_seconds_per_task} <span class="stat-unit">sec/task</span>`;
486
+ document.getElementById('rev-eta').textContent = `ETA: ${velocity.eta}`;
487
+ document.getElementById('rev-health').textContent =
488
+ debt.security_health_score;
489
+
490
+ const healthStatus = document.getElementById('rev-health-status');
491
+ healthStatus.textContent = `STATUS: ${debt.governance_status.toUpperCase()}`;
492
+ healthStatus.style.background =
493
+ debt.security_health_score > 80
494
+ ? 'rgba(63, 185, 80, 0.15)'
495
+ : 'rgba(248, 81, 73, 0.15)';
496
+ healthStatus.style.color =
497
+ debt.security_health_score > 80 ? 'var(--green)' : 'var(--red)';
498
+
499
+ document.getElementById('roi-hours').textContent = `${roi.hours_saved}h`;
500
+ document.getElementById('roi-gross').textContent = `$${roi.gross_value}`;
501
+ document.getElementById('roi-burn').textContent = `$${roi.token_cost}`;
502
+ document.getElementById('roi-total-pct').textContent =
503
+ `${roi.roi_percentage}%`;
504
+
505
+ document.getElementById('debt-critical').textContent = debt.critical_findings;
506
+ document.getElementById('debt-tier3').textContent = debt.tier3_approvals;
507
+ const riskBadge = document.getElementById('debt-risk');
508
+ riskBadge.textContent = debt.debt_level.toUpperCase();
509
+ riskBadge.className = `badge ${debt.debt_level === 'Minimal' ? 'badge-live' : ''}`;
510
+ if (debt.debt_level !== 'Minimal') riskBadge.style.color = 'var(--yellow)';
410
511
  }
411
512
 
412
513
  window.onresize = drawCharts;
@@ -421,9 +522,13 @@ document.querySelectorAll('nav .tab[data-page]').forEach((btn) => {
421
522
  btn.addEventListener('click', () => showPage(btn.dataset.page));
422
523
  });
423
524
  const __slider = document.getElementById('temporal-slider');
424
- if (__slider) __slider.addEventListener('input', (e) => onSliderChange(e.target.value));
525
+ if (__slider)
526
+ __slider.addEventListener('input', (e) => onSliderChange(e.target.value));
425
527
  document.querySelectorAll('[data-action]').forEach((el) => {
426
- const fns = { 'inject-hindsight': () => injectHindsight(), 'close-confirm': () => closeConfirm() };
528
+ const fns = {
529
+ 'inject-hindsight': () => injectHindsight(),
530
+ 'close-confirm': () => closeConfirm(),
531
+ };
427
532
  const fn = fns[el.dataset.action];
428
533
  if (fn) el.addEventListener('click', fn);
429
534
  });
@@ -5,49 +5,61 @@
5
5
  */
6
6
  'use strict';
7
7
 
8
- const Loader = require('./source-loader');
8
+ const Loader = require('./source-loader');
9
9
  const Generator = require('./skill-generator');
10
- const Scorer = require('./skill-scorer');
10
+ const Scorer = require('./skill-scorer');
11
11
  const Registrar = require('./skill-registrar');
12
12
 
13
13
  const ARGS = process.argv.slice(2);
14
14
  const SOURCE = ARGS[0];
15
- const NAME = ARGS[1] || 'new-skill';
15
+ const NAME = ARGS[1] || 'new-skill';
16
16
 
17
17
  if (!SOURCE) {
18
- console.log('Usage: node bin/skills-builder/learn-cli.js <source-url-or-path> [skill-name]');
18
+ console.log(
19
+ 'Usage: node bin/skills-builder/learn-cli.js <source-url-or-path> [skill-name]',
20
+ );
19
21
  process.exit(1);
20
22
  }
21
23
 
22
24
  async function main() {
23
25
  try {
24
26
  console.log(`\n📚 Learning skill: ${NAME} from ${SOURCE}`);
25
-
27
+
26
28
  // 1. Load
27
- const { content, metadata } = await Loader.load(SOURCE);
28
-
29
+ const { type, content, metadata } = await Loader.load(SOURCE);
30
+
29
31
  // 2. Generate
30
32
  const result = await Generator.generate({
31
33
  skillName: NAME,
32
34
  content,
33
35
  sourceMetadata: metadata,
34
- sessionId: 'cli-learn'
36
+ sessionId: 'cli-learn',
35
37
  });
36
-
38
+
37
39
  // 3. Score
38
40
  const scoreResult = Scorer.score(result.skillPath);
39
41
  console.log(`\n✅ Skill generated: ${result.skillPath}`);
40
- console.log(`⭐ Quality Score: ${scoreResult.quality_score}/100 (${scoreResult.threshold_status})`);
41
-
42
+ console.log(
43
+ `⭐ Quality Score: ${scoreResult.quality_score}/100 (${scoreResult.threshold_status})`,
44
+ );
45
+
42
46
  // 4. Register if acceptable
43
47
  if (scoreResult.can_register) {
44
- Registrar.register(result.skillPath, 'project');
48
+ Registrar.register({
49
+ skillName: NAME,
50
+ skillPath: result.skillPath,
51
+ tier: 'project',
52
+ qualityScore: scoreResult.quality_score,
53
+ sourceType: type,
54
+ source: SOURCE,
55
+ });
45
56
  console.log('📝 Registered in MANIFEST.md');
46
57
  } else {
47
58
  console.log('⚠️ Score too low for automatic registration.');
48
- scoreResult.improvement_suggestions.forEach(s => console.log(` - ${s}`));
59
+ scoreResult.improvement_suggestions.forEach((s) =>
60
+ console.log(` - ${s}`),
61
+ );
49
62
  }
50
-
51
63
  } catch (err) {
52
64
  console.error(`\n❌ Error: ${err.message}`);
53
65
  process.exit(1);
@@ -100,12 +100,28 @@ function releaseReadinessChecks(root) {
100
100
  pass: Boolean(pkgVersion) && pkgVersion === configVersion,
101
101
  fix: `Align versions: package.json=${pkgVersion || '?'} vs config.json=${configVersion || '?'}`,
102
102
  },
103
+ // CHECKS CONTENT, NOT MERE EXISTENCE, and that change has a cost worth naming: from now on every
104
+ // release needs a RELEASENOTES entry. Six published 11.x versions do not have one (11.0.0, 11.2.0,
105
+ // 11.2.1, 11.4.0, 11.7.1, 11.9.1) — the file has been curated rather than exhaustive. Those gaps are
106
+ // unaffected, because this only ever looks for the version being released.
107
+ //
108
+ // WHY IT CHANGED. This gate passed on `fileExists` while its changelog sibling three lines down
109
+ // checked that the version was IN the file. That asymmetry is why both prose surfaces shipped stale
110
+ // twice running: the immutable 11.9.3 tarball's README said "Latest release v11.9.2", and the
111
+ // immutable 11.9.4 tarball shipped with no 11.9.4 entry here at all — while README.md offers this
112
+ // file as the human-readable route to the BREAKING notes. 348a3a2c fixed the identical defect one
113
+ // release earlier by hand and added no gate, and it recurred immediately.
114
+ //
115
+ // Anchored, not `includes`. A substring test for "11.9.4" is also satisfied by "## v11.9.40" and by
116
+ // "## v11.9.4-notyet" — measured, when the equivalent test assertion was falsified.
103
117
  {
104
118
  id: 'releasenotes',
105
- label: 'RELEASENOTES.md present',
119
+ label: 'RELEASENOTES.md present and has an entry for the current version',
106
120
  points: 2,
107
- pass: fileExists(root, 'RELEASENOTES.md'),
108
- fix: 'Add RELEASENOTES.md for this release.',
121
+ pass: fileExists(root, 'RELEASENOTES.md') && Boolean(pkgVersion)
122
+ && new RegExp(`^## v${pkgVersion.replace(/\./g, '\\.')}(?![\\d.\\w-])`, 'm')
123
+ .test(safeRead(root, 'RELEASENOTES.md')),
124
+ fix: `Add a "## v${pkgVersion || '?'}" section to RELEASENOTES.md. README.md links here as the human-readable route to the BREAKING notes, so releasing without one points readers at a file whose newest entry describes a different version.`,
109
125
  },
110
126
  {
111
127
  id: 'changelog',