mindforge-cc 11.9.2 → 11.9.3

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 (94) hide show
  1. package/.agent/CLAUDE.md +37 -13
  2. package/.agent/hooks/mindforge-block-no-verify.js +61 -13
  3. package/.agent/hooks/mindforge-config-protection.js +82 -3
  4. package/.agent/hooks/mindforge-context-monitor.js +1 -1
  5. package/.agent/hooks/mindforge-workflow-guard.js +2 -2
  6. package/.agent/hooks/run-with-flags.js +190 -20
  7. package/.agent/mindforge/browse.md +2 -2
  8. package/.agent/mindforge/checkpoint.md +1 -1
  9. package/.agent/mindforge/harness-audit.md +1 -1
  10. package/.agent/mindforge/orch-add-feature.md +1 -1
  11. package/.agent/mindforge/orch-build-mvp.md +1 -1
  12. package/.agent/mindforge/orch-change-feature.md +1 -1
  13. package/.agent/mindforge/orch-fix-defect.md +1 -1
  14. package/.agent/mindforge/orch-refine-code.md +1 -1
  15. package/.agent/mindforge/qa.md +2 -2
  16. package/.claude/CLAUDE.md +37 -13
  17. package/.claude/commands/mindforge/browse.md +2 -2
  18. package/.claude/commands/mindforge/checkpoint.md +1 -1
  19. package/.claude/commands/mindforge/harness-audit.md +1 -1
  20. package/.claude/commands/mindforge/orch-add-feature.md +1 -1
  21. package/.claude/commands/mindforge/orch-build-mvp.md +1 -1
  22. package/.claude/commands/mindforge/orch-change-feature.md +1 -1
  23. package/.claude/commands/mindforge/orch-fix-defect.md +1 -1
  24. package/.claude/commands/mindforge/orch-refine-code.md +1 -1
  25. package/.claude/commands/mindforge/qa.md +2 -2
  26. package/.mindforge/MINDFORGE-SCHEMA.json +1 -1
  27. package/.mindforge/config.json +3 -3
  28. package/.mindforge/engine/autonomous/headless-adapter.md +2 -2
  29. package/.mindforge/engine/temporal-protocol.md +2 -2
  30. package/.mindforge/governance/change-classifier.md +20 -4
  31. package/.mindforge/memory/sync-manifest.json +1 -1
  32. package/.mindforge/skills/agent-architecture-audit/SKILL.md +2 -2
  33. package/.mindforge/skills/orch-pipeline/SKILL.md +4 -4
  34. package/CHANGELOG.md +194 -0
  35. package/MINDFORGE.md +13 -6
  36. package/README.md +4 -3
  37. package/RELEASENOTES.md +2 -2
  38. package/SECURITY.md +22 -3
  39. package/bin/autonomous/auto-runner.js +65 -2
  40. package/bin/change-classifier.js +151 -16
  41. package/bin/dashboard/api-router.js +18 -38
  42. package/bin/dashboard/frontend/app.js +429 -0
  43. package/bin/dashboard/frontend/index.html +13 -406
  44. package/bin/dashboard/metrics-aggregator.js +46 -22
  45. package/bin/dashboard/server.js +160 -1
  46. package/bin/dashboard/sse-bridge.js +11 -8
  47. package/bin/engine/sre-manager.js +1 -1
  48. package/bin/engine/temporal-cli.js +56 -6
  49. package/bin/engine/verification-runner.js +134 -17
  50. package/bin/engine/verify-cli.js +25 -7
  51. package/bin/governance/approval-record.js +147 -0
  52. package/bin/governance/approve.js +12 -7
  53. package/bin/governance/policy-engine.js +33 -3
  54. package/bin/governance/policy-gate-hardened.js +36 -1
  55. package/bin/governance/verify-approvals.js +163 -0
  56. package/bin/harness-audit.js +224 -10
  57. package/bin/hooks/instinct-capture-hook.js +12 -4
  58. package/bin/install.js +63 -3
  59. package/bin/installer/harness-adapter-compliance.js +339 -28
  60. package/bin/installer/hook-registration.js +504 -0
  61. package/bin/installer-core.js +451 -63
  62. package/bin/learning/instinct-cli.js +7 -0
  63. package/bin/memory/vector-hub.js +196 -13
  64. package/bin/migrations/0.6.0-to-1.0.0.js +30 -25
  65. package/bin/migrations/1.0.0-to-2.0.0.js +22 -23
  66. package/bin/mindforge-cli.js +67 -6
  67. package/bin/models/cost-tracker.js +104 -6
  68. package/bin/models/model-client.js +6 -1
  69. package/bin/revops/debt-monitor.js +57 -13
  70. package/bin/security/trust-gate-hook.js +50 -6
  71. package/bin/skill-validator.js +6 -1
  72. package/bin/skills-builder/skill-scorer.js +46 -6
  73. package/bin/updater/self-update.js +6 -1
  74. package/bin/updater/version-comparator.js +21 -1
  75. package/bin/utils/mindforge-version.js +99 -0
  76. package/bin/utils/redact-secrets.js +106 -0
  77. package/bin/validate-config.js +42 -2
  78. package/bin/wizard/setup-wizard.js +4 -1
  79. package/bin/wizard/theme.js +9 -1
  80. package/changelogs/index.json +11 -9
  81. package/changelogs/v11.9.3.md +195 -0
  82. package/docs/References/config-reference.md +5 -2
  83. package/docs/References/sdk-api.md +1 -1
  84. package/docs/Templates/Codebase/architecture.md +1 -1
  85. package/docs/commands-reference.md +4 -5
  86. package/docs/faq.md +25 -5
  87. package/docs/getting-started.md +3 -3
  88. package/docs/sdk-reference.md +15 -7
  89. package/docs/troubleshooting.md +10 -6
  90. package/docs/user-guide.md +14 -14
  91. package/examples/sdk-integration/README.md +1 -1
  92. package/package.json +7 -3
  93. package/subagents/.claude-plugin/marketplace.json +1 -1
  94. package/bin/dashboard/approval-handler.js +0 -136
@@ -154,17 +154,17 @@
154
154
  <div class="logo-text">MindForge <span style="color:var(--muted); font-weight:400">Dash</span></div>
155
155
  <div id="project-name" class="badge">PROJ: UNSET</div>
156
156
  </div>
157
- <div id="connection-status" class="badge badge-live">● Connected</div>
157
+ <div id="connection-status" class="badge">● Connecting…</div>
158
158
  </header>
159
159
 
160
160
  <nav>
161
- <button class="tab active" onclick="showPage('activity')">Activity</button>
162
- <button class="tab" onclick="showPage('metrics')">Metrics</button>
163
- <button class="tab" onclick="showPage('approvals')">Approvals</button>
164
- <button class="tab" onclick="showPage('temporal')">Temporal</button>
165
- <button class="tab" onclick="showPage('revops')">RevOps</button>
166
- <button class="tab" onclick="showPage('memory')">Memory</button>
167
- <button class="tab" onclick="showPage('team')">Team</button>
161
+ <button class="tab active" data-page="activity">Activity</button>
162
+ <button class="tab" data-page="metrics">Metrics</button>
163
+ <button class="tab" data-page="approvals">Approvals</button>
164
+ <button class="tab" data-page="temporal">Temporal</button>
165
+ <button class="tab" data-page="revops">RevOps</button>
166
+ <button class="tab" data-page="memory">Memory</button>
167
+ <button class="tab" data-page="team">Team</button>
168
168
  </nav>
169
169
 
170
170
  <main>
@@ -179,7 +179,7 @@
179
179
  <div class="card">
180
180
  <div class="card-header">
181
181
  <div class="card-title">Live Audit Feed</div>
182
- <div style="font-size:10px; color:var(--muted)">SSE STREAMING ACTIVE</div>
182
+ <div style="font-size:10px; color:var(--muted)">SSE: connecting…</div>
183
183
  </div>
184
184
  <div id="audit-feed"></div>
185
185
  </div>
@@ -224,7 +224,7 @@
224
224
  </div>
225
225
  <div class="card-body">
226
226
  <div style="margin-bottom: 24px;">
227
- <input type="range" id="temporal-slider" style="width:100%; height:8px; accent-color:var(--accent);" min="0" max="0" step="1" oninput="onSliderChange(this.value)">
227
+ <input type="range" id="temporal-slider" style="width:100%; height:8px; accent-color:var(--accent);" min="0" max="0" step="1">
228
228
  <div style="display:flex; justify-content:space-between; margin-top:8px; font-family:var(--font-mono); font-size:10px; color:var(--muted)">
229
229
  <span id="slider-start">Past</span>
230
230
  <span id="slider-current">Select Snapshot</span>
@@ -247,7 +247,7 @@
247
247
  <label style="display:block; font-size:11px; margin-bottom:8px; color:var(--muted); text-transform:uppercase;">Steering Instruction:</label>
248
248
  <textarea id="steering-input" class="confirm-input" style="height:120px; resize:none;" placeholder="e.g. 'Rewind to step 2 and ignore the legacy auth module...'"></textarea>
249
249
  </div>
250
- <button id="inject-btn" class="btn btn-approve" style="width:100%; justify-content:center;" onclick="injectHindsight()" disabled>Rewind & Inject Fix</button>
250
+ <button id="inject-btn" class="btn btn-approve" style="width:100%; justify-content:center;" data-action="inject-hindsight" disabled>Rewind & Inject Fix</button>
251
251
  </div>
252
252
  </div>
253
253
  </div>
@@ -364,405 +364,12 @@
364
364
  <input type="text" id="confirm-input" class="confirm-input" placeholder="e.g. 1.1-auth_fix">
365
365
  </div>
366
366
  <div style="display:flex; gap:12px; justify-content:flex-end;">
367
- <button class="btn" style="background:var(--border)" onclick="closeConfirm()">Cancel</button>
367
+ <button class="btn" style="background:var(--border)" data-action="close-confirm">Cancel</button>
368
368
  <button id="confirm-btn" class="btn btn-approve">Proceed</button>
369
369
  </div>
370
370
  </div>
371
371
  </div>
372
372
 
373
- <script>
374
- // ── State ─────────────────────────────────────────────────────────────────
375
- let currentApprovals = [];
376
- let state = { costs: [], quality: [] };
377
- let currentTier3Approval = null;
378
-
379
- function showPage(id) {
380
- document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
381
- document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
382
- document.getElementById(id).classList.add('active');
383
- const tab = Array.from(document.querySelectorAll('.tab')).find(t => t.innerText.toLowerCase() === id.toLowerCase());
384
- if (tab) tab.classList.add('active');
385
-
386
- if (id === 'metrics') drawCharts();
387
- }
388
-
389
- function escapeHtml(str) {
390
- if (!str) return '';
391
- return String(str).replace(/[&<>"']/g, m => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;' }[m]));
392
- }
393
-
394
- // ── WebSocket / SSE ───────────────────────────────────────────────────────
395
- const es = new EventSource('/events');
396
- const feed = document.getElementById('audit-feed');
397
-
398
- es.onopen = () => {
399
- const el = document.getElementById('connection-status');
400
- el.textContent = '● Connected';
401
- el.style.color = 'var(--green)';
402
- refreshData();
403
- };
404
-
405
- es.onerror = () => {
406
- const el = document.getElementById('connection-status');
407
- el.textContent = '● Disconnected';
408
- el.style.color = 'var(--red)';
409
- };
410
-
411
- es.addEventListener('status:update', (e) => {
412
- const data = JSON.parse(e.data);
413
- updateStatusUI(data);
414
- });
415
-
416
- es.addEventListener('audit:new', (e) => {
417
- const data = JSON.parse(e.data);
418
- appendAuditEvent(data);
419
- });
420
-
421
- es.addEventListener('approval:new', (e) => {
422
- const data = JSON.parse(e.data);
423
- refreshApprovals();
424
- });
425
-
426
- // ── API Interactions ──────────────────────────────────────────────────────
427
- async function refreshData() {
428
- try {
429
- const res = await fetch('/api/metrics');
430
- const data = await res.json();
431
- state = data;
432
- updateMetricsUI(data);
433
- refreshCosts();
434
- refreshApprovals();
435
- refreshMemory();
436
- refreshTeam();
437
- refreshRevOps();
438
- if (document.getElementById('temporal').classList.contains('active')) {
439
- refreshTemporal();
440
- }
441
- } catch(e) { console.error('Refresh fail', e); }
442
- }
443
-
444
- async function refreshRevOps() {
445
- try {
446
- const res = await fetch('/api/revops/overview');
447
- const data = await res.json();
448
- if (data.success) {
449
- updateRevOpsUI(data);
450
- }
451
- } catch(e) {}
452
- }
453
-
454
- let temporalHistory = [];
455
- async function refreshTemporal() {
456
- try {
457
- const res = await fetch('/api/temporal/history');
458
- const data = await res.json();
459
- temporalHistory = data;
460
- renderTemporalTimeline();
461
- } catch(e) {}
462
- }
463
-
464
- async function refreshApprovals() {
465
- try {
466
- const res = await fetch('/api/approvals');
467
- const data = await res.json();
468
- currentApprovals = data;
469
- renderApprovals();
470
- } catch(e) {}
471
- }
472
-
473
- async function refreshMemory() {
474
- try {
475
- const res = await fetch('/api/memory');
476
- const data = await res.json();
477
- document.getElementById('memory-map').innerHTML = `<pre>${escapeHtml(JSON.stringify(data.graph || {}, null, 2))}</pre>`;
478
- document.getElementById('memory-stats-list').innerHTML = `<div class="stat-value">${data.count || 0} <span class="stat-unit">Items Indexed</span></div>`;
479
- } catch(e) {}
480
- }
481
-
482
- async function refreshTeam() {
483
- try {
484
- const res = await fetch('/api/team');
485
- const data = await res.json();
486
- renderTeam(data);
487
- } catch(e) {}
488
- }
489
-
490
- async function decide(id, decision) {
491
- const approval = currentApprovals.find(a => a.id === id);
492
- if (!approval) return;
493
-
494
- if (decision === 'approve' && approval.tier === 3) {
495
- currentTier3Approval = approval;
496
- const expected = `${approval.phase}-${approval.plan}`;
497
- document.getElementById('confirm-title').innerText = 'Critical Tier 3 Approval';
498
- document.getElementById('confirm-desc').innerText = `Danger: You are approving a sensitive change (Phase ${approval.phase}, Plan ${approval.plan}). This requires manual confirmation.`;
499
- document.getElementById('tier3-input-group').style.display = 'block';
500
- document.getElementById('confirm-input').value = '';
501
- document.getElementById('confirm-overlay').style.display = 'flex';
502
- document.getElementById('confirm-btn').onclick = () => submitDecision(id, decision, document.getElementById('confirm-input').value);
503
- return;
504
- }
505
-
506
- submitDecision(id, decision);
507
- }
508
-
509
- async function submitDecision(id, decision, confirmationId = null) {
510
- try {
511
- const res = await fetch(`/api/approve/${id}`, {
512
- method: 'POST',
513
- headers: { 'Content-Type': 'application/json' },
514
- body: JSON.stringify({
515
- decision,
516
- comment: 'Decided via Dashboard',
517
- approver: 'admin-dash',
518
- confirmation_id: confirmationId
519
- })
520
- });
521
- const result = await res.json();
522
- if (!result.success) alert(result.error);
523
- closeConfirm();
524
- refreshApprovals();
525
- } catch(e) { alert('Action failed'); }
526
- }
527
-
528
- function closeConfirm() {
529
- document.getElementById('confirm-overlay').style.display = 'none';
530
- }
531
-
532
- // ── UI Rendering ──────────────────────────────────────────────────────────
533
- function updateStatusUI(data) {
534
- document.getElementById('project-name').textContent = `PROJ: ${data.project_name || 'UNSET'}`;
535
- document.getElementById('stat-phase').textContent = data.phase || '0';
536
- document.getElementById('stat-status').textContent = (data.auto_status || 'IDLE').toUpperCase();
537
- document.getElementById('stat-tasks').textContent = `${data.tasks_completed || 0}/${data.tasks_total || 0}`;
538
- document.getElementById('stat-elapsed').textContent = data.elapsed_ms ? (data.elapsed_ms / 1000).toFixed(1) + 's' : '0s';
539
- }
540
-
541
- function updateMetricsUI(data) {
542
- // /api/metrics returns { sessions, avg_quality, avg_cost_usd, ... } — see
543
- // bin/dashboard/metrics-aggregator.js getMetrics(). It has never returned
544
- // `costs` or `quality`, so these tiles used to render "$0.0000" and "NaN".
545
- document.getElementById('stat-avg-quality').textContent = (data.avg_quality ?? 0).toFixed(1);
546
- }
547
-
548
- // Cumulative spend comes from the usage ledger via /api/costs (already wired
549
- // in bin/dashboard/api-router.js but never fetched until now).
550
- async function refreshCosts() {
551
- try {
552
- const res = await fetch('/api/costs?window=7');
553
- // sendServerError returns a well-formed JSON body, so res.json() would
554
- // SUCCEED on a 500 and `total_usd ?? 0` would render "$0.00" — an outage
555
- // and genuine zero spend look identical under the "Cost (7d)" label.
556
- if (!res.ok) throw new Error(`/api/costs ${res.status}`);
557
- const data = await res.json();
558
- document.getElementById('stat-total-cost').textContent = `${(data.total_usd ?? 0).toFixed(2)}`;
559
- } catch (e) { console.error('Cost refresh failed', e); }
560
- }
561
-
562
- function appendAuditEvent(data) {
563
- const div = document.createElement('div');
564
- div.className = `event type-${data.event}`;
565
- const time = new Date(data.timestamp).toLocaleTimeString([], { hour12:false, hour:'2-digit', minute:'2-digit', second:'2-digit' });
566
- 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>`;
567
- feed.prepend(div);
568
- if (feed.children.length > 200) feed.lastChild.remove();
569
- }
570
-
571
- function renderApprovals() {
572
- const list = document.getElementById('approval-list');
573
- if (currentApprovals.length === 0) {
574
- list.innerHTML = '<div style="text-align:center; padding: 40px; color:var(--muted)">No pending approvals</div>';
575
- return;
576
- }
577
-
578
- list.innerHTML = currentApprovals.map(a => `
579
- <div class="card approval-card">
580
- <div class="card-body">
581
- <div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:12px;">
582
- <div>
583
- <span class="tier-badge tier-${a.tier}">Tier ${a.tier}</span>
584
- <span style="font-weight:700">Phase ${a.phase} | ${a.plan}</span>
585
- </div>
586
- <div style="font-size:11px; color:var(--muted)">Expires: ${new Date(a.expires_at).toLocaleTimeString()}</div>
587
- </div>
588
- <p style="margin-bottom:16px; font-size:12px; line-height:1.4">${escapeHtml(a.summary || 'No summary provided')}</p>
589
- <div style="display:flex; gap:12px;">
590
- <button class="btn btn-approve" onclick="decide('${a.id}', 'approve')">Approve Selection</button>
591
- <button class="btn btn-reject" onclick="decide('${a.id}', 'reject')">Reject</button>
592
- </div>
593
- </div>
594
- </div>
595
- `).join('');
596
- }
597
-
598
- function renderTeam(data) {
599
- const list = document.getElementById('team-activity-list');
600
- if (!data || data.length === 0) {
601
- list.innerHTML = '<div style="text-align:center; padding: 40px; color:var(--muted)">No recent team activity</div>';
602
- return;
603
- }
604
- list.innerHTML = data.map(ev => `
605
- <div class="activity-row">
606
- <div class="avatar">${(ev.user || '?').charAt(0).toUpperCase()}</div>
607
- <div class="activity-info">
608
- <div><span class="activity-user">${escapeHtml(ev.user)}</span> <span class="activity-action">${escapeHtml(ev.action)}</span></div>
609
- <div class="activity-time">${new Date(ev.timestamp).toLocaleString()}</div>
610
- </div>
611
- </div>
612
- `).join('');
613
- }
614
-
615
- // ── Simple Charts Implementation ──────────────────────────────────────────
616
- function drawCharts() {
617
- drawCanvasChart('chart-costs', state.costs?.map(c => c.cost) || [], varColor('--accent'));
618
- drawCanvasChart('chart-quality', state.quality?.map(q => q.score) || [], varColor('--green'), 100);
619
- }
620
-
621
- function varColor(name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); }
622
-
623
- function drawCanvasChart(id, data, color, maxVal = null) {
624
- const canvas = document.getElementById(id);
625
- if (!canvas) return;
626
- const ctx = canvas.getContext('2d');
627
- const dpr = window.devicePixelRatio || 1;
628
- canvas.width = canvas.offsetWidth * dpr;
629
- canvas.height = canvas.offsetHeight * dpr;
630
- ctx.scale(dpr, dpr);
631
-
632
- const w = canvas.offsetWidth;
633
- const h = canvas.offsetHeight;
634
- ctx.clearRect(0,0,w,h);
635
-
636
- if (data.length < 2) return;
637
-
638
- const max = maxVal || Math.max(...data) * 1.2 || 1;
639
- const step = w / (data.length - 1);
640
-
641
- ctx.beginPath();
642
- ctx.strokeStyle = color;
643
- ctx.lineWidth = 2;
644
- ctx.lineJoin = 'round';
645
-
646
- data.forEach((val, i) => {
647
- const x = i * step;
648
- const y = h - (val / max) * h;
649
- if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
650
- });
651
- ctx.stroke();
652
-
653
- // Area fill
654
- ctx.lineTo(w, h); ctx.lineTo(0, h); ctx.closePath();
655
- ctx.fillStyle = color.replace(')', ', 0.1)').replace('rgb', 'rgba');
656
- ctx.fill();
657
- }
658
-
659
- // ── Temporal Steering Logic ────────────────────────────────────────────────
660
- function renderTemporalTimeline() {
661
- const slider = document.getElementById('temporal-slider');
662
- const count = document.getElementById('timeline-count');
663
-
664
- if (temporalHistory.length === 0) {
665
- slider.max = 0;
666
- count.textContent = '0 Snapshots';
667
- return;
668
- }
669
-
670
- slider.max = temporalHistory.length - 1;
671
- count.textContent = `${temporalHistory.length} Snapshots`;
672
-
673
- // Default to latest if not touching
674
- if (slider.value == 0 && temporalHistory.length > 0) {
675
- onSliderChange(temporalHistory.length - 1);
676
- slider.value = temporalHistory.length - 1;
677
- }
678
- }
679
-
680
- let selectedSnapshot = null;
681
- async function onSliderChange(index) {
682
- const snap = temporalHistory[temporalHistory.length - 1 - index]; // reversed for chronological range
683
- if (!snap) return;
684
-
685
- selectedSnapshot = snap;
686
- document.getElementById('slider-current').textContent = `Point: ${snap.id.slice(0, 8)} (${new Date(snap.timestamp).toLocaleTimeString()})`;
687
- document.getElementById('inject-btn').disabled = false;
688
-
689
- // Fetch sample audit file for this snapshot
690
- try {
691
- const res = await fetch(`/api/temporal/snapshot/${snap.id}/AUDIT.jsonl`);
692
- const content = await res.text();
693
- document.getElementById('snapshot-viewer').textContent = content || 'No audit log available for this point.';
694
- } catch(e) {
695
- document.getElementById('snapshot-viewer').textContent = 'Failed to load snapshot details.';
696
- }
697
- }
698
-
699
- async function injectHindsight() {
700
- if (!selectedSnapshot) return;
701
- const instruction = document.getElementById('steering-input').value;
702
- if (!instruction) {
703
- alert('Please provide a steering instruction for the hindsight injection.');
704
- return;
705
- }
706
-
707
- const btn = document.getElementById('inject-btn');
708
- btn.disabled = true;
709
- btn.textContent = 'Injecting Steering Vector...';
710
-
711
- try {
712
- const res = await fetch('/api/temporal/inject', {
713
- method: 'POST',
714
- headers: { 'Content-Type': 'application/json' },
715
- body: JSON.stringify({
716
- auditId: selectedSnapshot.id,
717
- fixDescription: instruction
718
- })
719
- });
720
- const result = await res.json();
721
- if (result.success) {
722
- alert('Hindsight Injection Successful. Agent state rolled back and awaiting re-optimization.');
723
- showPage('activity');
724
- } else {
725
- alert('Injection failed: ' + result.error);
726
- }
727
- } catch(e) {
728
- alert('Failed to connect to temporal engine.');
729
- } finally {
730
- btn.disabled = false;
731
- btn.textContent = 'Rewind & Inject Fix';
732
- }
733
- }
734
-
735
- // ── RevOps Logic ─────────────────────────────────────────────────────────
736
- function updateRevOpsUI(data) {
737
- const { roi, velocity, debt } = data;
738
-
739
- document.getElementById('rev-roi').textContent = `${roi.roi_percentage}%`;
740
- document.getElementById('rev-net').textContent = `$${roi.net_value}`;
741
- document.getElementById('rev-velocity').innerHTML = `${velocity.avg_seconds_per_task} <span class="stat-unit">sec/task</span>`;
742
- document.getElementById('rev-eta').textContent = `ETA: ${velocity.eta}`;
743
- document.getElementById('rev-health').textContent = debt.security_health_score;
744
-
745
- const healthStatus = document.getElementById('rev-health-status');
746
- healthStatus.textContent = `STATUS: ${debt.governance_status.toUpperCase()}`;
747
- healthStatus.style.background = debt.security_health_score > 80 ? 'rgba(63, 185, 80, 0.15)' : 'rgba(248, 81, 73, 0.15)';
748
- healthStatus.style.color = debt.security_health_score > 80 ? 'var(--green)' : 'var(--red)';
749
-
750
- document.getElementById('roi-hours').textContent = `${roi.hours_saved}h`;
751
- document.getElementById('roi-gross').textContent = `$${roi.gross_value}`;
752
- document.getElementById('roi-burn').textContent = `$${roi.token_cost}`;
753
- document.getElementById('roi-total-pct').textContent = `${roi.roi_percentage}%`;
754
-
755
- document.getElementById('debt-critical').textContent = debt.critical_findings;
756
- document.getElementById('debt-tier3').textContent = debt.tier3_approvals;
757
- const riskBadge = document.getElementById('debt-risk');
758
- riskBadge.textContent = debt.debt_level.toUpperCase();
759
- riskBadge.className = `badge ${debt.debt_level === 'Minimal' ? 'badge-live' : ''}`;
760
- if (debt.debt_level !== 'Minimal') riskBadge.style.color = 'var(--yellow)';
761
- }
762
-
763
- window.onresize = drawCharts;
764
- setInterval(refreshData, 10000); // More frequent refresh for live dashboard
765
- showPage('activity');
766
- </script>
373
+ <script src="app.js" defer></script>
767
374
  </body>
768
375
  </html>
@@ -183,36 +183,60 @@ function getMetrics() {
183
183
  }
184
184
 
185
185
  // ── Approvals ─────────────────────────────────────────────────────────────────
186
+ /**
187
+ * Read the approval records that actually exist, with their verified integrity.
188
+ *
189
+ * This replaces a read path that could never return anything. It filtered
190
+ * `f.startsWith('APPROVAL-')` while the only producer — bin/governance/approve.js — writes
191
+ * `approval-<id>.json` in lower case, and String.prototype.startsWith is case-sensitive on every
192
+ * platform (macOS's case-insensitive filesystem affects fs.open, not this comparison). It then
193
+ * categorised on a `status` field no producer has ever written, so any record that HAD been
194
+ * found would have fallen into `pending` regardless of what it said.
195
+ *
196
+ * The categories are now derived from verification rather than from a self-declared status:
197
+ * a record either verifies against the release being built, or it is stale, or it is corrupt.
198
+ * There is deliberately no `pending` category — nothing in MindForge creates a pending approval
199
+ * REQUEST; approve.js records an already-made decision. A category with no producer is exactly
200
+ * the kind of empty promise this pass exists to remove.
201
+ *
202
+ * Returns an ARRAY, because the previous object shape was also incompatible with its only
203
+ * consumer: the dashboard did `currentApprovals.length` and `.map()` on it, which on an object
204
+ * yields undefined and then a TypeError.
205
+ */
186
206
  function getApprovals() {
187
207
  const paths = getPaths();
188
- if (!fs.existsSync(paths.approvals)) return { pending: [], approved: [], rejected: [], expired: [] };
208
+ if (!fs.existsSync(paths.approvals)) return [];
189
209
 
190
- const now = Date.now();
210
+ const { verifyRecord } = require('../governance/approval-record');
211
+ let currentVersion = null;
212
+ try {
213
+ currentVersion = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf8')).version;
214
+ } catch { /* version binding is skipped if the manifest is unreadable */ }
215
+
216
+ const now = Date.now();
191
217
  const files = fs.readdirSync(paths.approvals)
192
- .filter(f => f.startsWith('APPROVAL-') && f.endsWith('.json'))
218
+ .filter(f => f.endsWith('.json'))
193
219
  .sort();
194
220
 
195
- const pending = [];
196
- const approved = [];
197
- const rejected = [];
198
- const expired = [];
199
-
221
+ const out = [];
200
222
  for (const f of files) {
201
- try {
202
- const data = JSON.parse(fs.readFileSync(path.join(paths.approvals, f), 'utf8'));
203
- const expiry = data.expires_at ? new Date(data.expires_at).getTime() : Infinity;
204
- const hoursRemaining = expiry === Infinity ? null : (expiry - now) / 3_600_000;
205
-
206
- const enriched = { ...data, hours_remaining: hoursRemaining };
207
-
208
- if (data.status === 'approved') approved.push(enriched);
209
- else if (data.status === 'rejected') rejected.push(enriched);
210
- else if (expiry < now) expired.push({ ...enriched, status: 'expired' });
211
- else pending.push(enriched);
212
- } catch { /* skip corrupt files */ }
223
+ let data;
224
+ try { data = JSON.parse(fs.readFileSync(path.join(paths.approvals, f), 'utf8')); }
225
+ catch (e) {
226
+ out.push({ file: f, state: 'corrupt', problems: [`not valid JSON: ${e.message}`] });
227
+ continue;
228
+ }
229
+ const v = verifyRecord(data, { currentVersion, now: new Date(now) });
230
+ const expiry = data.expires_at ? new Date(data.expires_at).getTime() : null;
231
+ out.push({
232
+ ...data,
233
+ file: f,
234
+ state: v.ok ? 'valid' : (v.stale ? 'stale' : 'corrupt'),
235
+ problems: v.problems,
236
+ hours_remaining: expiry === null ? null : (expiry - now) / 3_600_000,
237
+ });
213
238
  }
214
-
215
- return { pending, approved, rejected, expired };
239
+ return out;
216
240
  }
217
241
 
218
242
  // ── Team activity ─────────────────────────────────────────────────────────────