mindforge-cc 11.9.1 → 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 (121) 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/consult.md +1 -1
  10. package/.agent/mindforge/cost-report.md +1 -1
  11. package/.agent/mindforge/harness-audit.md +1 -1
  12. package/.agent/mindforge/orch-add-feature.md +1 -1
  13. package/.agent/mindforge/orch-build-mvp.md +1 -1
  14. package/.agent/mindforge/orch-change-feature.md +1 -1
  15. package/.agent/mindforge/orch-fix-defect.md +1 -1
  16. package/.agent/mindforge/orch-refine-code.md +1 -1
  17. package/.agent/mindforge/qa.md +2 -2
  18. package/.claude/CLAUDE.md +37 -13
  19. package/.claude/commands/mindforge/browse.md +2 -2
  20. package/.claude/commands/mindforge/checkpoint.md +1 -1
  21. package/.claude/commands/mindforge/consult.md +1 -1
  22. package/.claude/commands/mindforge/cost-report.md +1 -1
  23. package/.claude/commands/mindforge/harness-audit.md +1 -1
  24. package/.claude/commands/mindforge/orch-add-feature.md +1 -1
  25. package/.claude/commands/mindforge/orch-build-mvp.md +1 -1
  26. package/.claude/commands/mindforge/orch-change-feature.md +1 -1
  27. package/.claude/commands/mindforge/orch-fix-defect.md +1 -1
  28. package/.claude/commands/mindforge/orch-refine-code.md +1 -1
  29. package/.claude/commands/mindforge/qa.md +2 -2
  30. package/.mindforge/MINDFORGE-SCHEMA.json +126 -13
  31. package/.mindforge/config.json +4 -4
  32. package/.mindforge/engine/autonomous/headless-adapter.md +2 -2
  33. package/.mindforge/engine/cost-tracking/router.md +1 -1
  34. package/.mindforge/engine/cost-tracking/token-ledger.md +21 -24
  35. package/.mindforge/engine/temporal-protocol.md +2 -2
  36. package/.mindforge/governance/change-classifier.md +20 -4
  37. package/.mindforge/memory/sync-manifest.json +1 -1
  38. package/.mindforge/metrics/METRICS-SCHEMA.md +13 -4
  39. package/.mindforge/personas/cost-optimizer.md +2 -2
  40. package/.mindforge/personas/multi-model-bridge.md +1 -1
  41. package/.mindforge/skills/agent-architecture-audit/SKILL.md +2 -2
  42. package/.mindforge/skills/cost-aware-routing/SKILL.md +3 -3
  43. package/.mindforge/skills/multi-llm-consult/SKILL.md +2 -2
  44. package/.mindforge/skills/orch-pipeline/SKILL.md +4 -4
  45. package/CHANGELOG.md +402 -0
  46. package/MINDFORGE.md +13 -6
  47. package/README.md +51 -2
  48. package/RELEASENOTES.md +55 -2
  49. package/SECURITY.md +22 -3
  50. package/bin/autonomous/audit-writer.js +48 -33
  51. package/bin/autonomous/auto-runner.js +65 -2
  52. package/bin/change-classifier.js +151 -16
  53. package/bin/dashboard/api-router.js +28 -47
  54. package/bin/dashboard/error-response.js +44 -0
  55. package/bin/dashboard/frontend/app.js +429 -0
  56. package/bin/dashboard/frontend/index.html +14 -390
  57. package/bin/dashboard/metrics-aggregator.js +75 -30
  58. package/bin/dashboard/revops-api.js +12 -2
  59. package/bin/dashboard/server.js +245 -6
  60. package/bin/dashboard/sse-bridge.js +11 -8
  61. package/bin/dashboard/temporal-api.js +11 -5
  62. package/bin/engine/remediation-engine.js +12 -1
  63. package/bin/engine/sre-manager.js +1 -1
  64. package/bin/engine/temporal-cli.js +56 -6
  65. package/bin/engine/temporal-hub.js +41 -9
  66. package/bin/engine/verification-runner.js +134 -17
  67. package/bin/engine/verify-cli.js +25 -7
  68. package/bin/eval/eval-harness.js +212 -1
  69. package/bin/eval/golden-set-retrieval.json +9 -0
  70. package/bin/governance/approval-record.js +147 -0
  71. package/bin/governance/approve.js +12 -7
  72. package/bin/governance/policy-engine.js +41 -3
  73. package/bin/governance/policy-gate-hardened.js +36 -1
  74. package/bin/governance/verify-approvals.js +163 -0
  75. package/bin/harness-audit.js +224 -10
  76. package/bin/hindsight-injector.js +8 -2
  77. package/bin/hooks/instinct-capture-hook.js +19 -5
  78. package/bin/install.js +63 -3
  79. package/bin/installer/harness-adapter-compliance.js +339 -28
  80. package/bin/installer/hook-registration.js +504 -0
  81. package/bin/installer-core.js +451 -63
  82. package/bin/learning/instinct-cli.js +14 -24
  83. package/bin/memory/knowledge-capture.js +23 -3
  84. package/bin/memory/knowledge-graph.js +70 -31
  85. package/bin/memory/vector-hub.js +500 -44
  86. package/bin/migrations/0.6.0-to-1.0.0.js +30 -25
  87. package/bin/migrations/1.0.0-to-2.0.0.js +22 -23
  88. package/bin/mindforge-cli.js +110 -17
  89. package/bin/models/cost-tracker.js +126 -29
  90. package/bin/models/model-client.js +6 -1
  91. package/bin/models/model-router.js +28 -7
  92. package/bin/models/usage-record.js +71 -0
  93. package/bin/revops/debt-monitor.js +57 -13
  94. package/bin/security/trust-gate-hook.js +50 -6
  95. package/bin/skill-validator.js +6 -1
  96. package/bin/skills-builder/skill-scorer.js +46 -6
  97. package/bin/updater/self-update.js +6 -1
  98. package/bin/updater/version-comparator.js +21 -1
  99. package/bin/utils/file-lock.js +106 -0
  100. package/bin/utils/mindforge-params.js +124 -0
  101. package/bin/utils/mindforge-version.js +99 -0
  102. package/bin/utils/redact-secrets.js +106 -0
  103. package/bin/validate-config.js +75 -17
  104. package/bin/wizard/setup-wizard.js +4 -1
  105. package/bin/wizard/theme.js +9 -1
  106. package/changelogs/index.json +11 -9
  107. package/changelogs/v11.9.2.md +209 -0
  108. package/changelogs/v11.9.3.md +195 -0
  109. package/docs/References/config-reference.md +76 -14
  110. package/docs/References/sdk-api.md +1 -1
  111. package/docs/Templates/Codebase/architecture.md +1 -1
  112. package/docs/commands-reference.md +4 -5
  113. package/docs/faq.md +25 -5
  114. package/docs/getting-started.md +3 -3
  115. package/docs/sdk-reference.md +15 -7
  116. package/docs/troubleshooting.md +10 -6
  117. package/docs/user-guide.md +14 -14
  118. package/examples/sdk-integration/README.md +1 -1
  119. package/package.json +10 -4
  120. package/subagents/.claude-plugin/marketplace.json +1 -1
  121. 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>
@@ -198,7 +198,7 @@
198
198
  </div>
199
199
  </div>
200
200
  <div class="grid-4">
201
- <div class="card"><div class="card-body"><div class="stat-label">Cumulative Cost</div><div id="stat-total-cost" class="stat-value">$0.00</div></div></div>
201
+ <div class="card"><div class="card-body"><div class="stat-label">Cost (7d)</div><div id="stat-total-cost" class="stat-value">$0.00</div></div></div>
202
202
  <div class="card"><div class="card-body"><div class="stat-label">Avg Quality</div><div id="stat-avg-quality" class="stat-value">0.0</div></div></div>
203
203
  <div class="card"><div class="card-body"><div class="stat-label">LSPs Registered</div><div id="stat-lsp-count" class="stat-value">0</div></div></div>
204
204
  <div class="card"><div class="card-body"><div class="stat-label">Token Velocity</div><div id="stat-velocity" class="stat-value">0 <span class="stat-unit">tx/m</span></div></div></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,388 +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
- refreshApprovals();
434
- refreshMemory();
435
- refreshTeam();
436
- refreshRevOps();
437
- if (document.getElementById('temporal').classList.contains('active')) {
438
- refreshTemporal();
439
- }
440
- } catch(e) { console.error('Refresh fail', e); }
441
- }
442
-
443
- async function refreshRevOps() {
444
- try {
445
- const res = await fetch('/api/revops/overview');
446
- const data = await res.json();
447
- if (data.success) {
448
- updateRevOpsUI(data);
449
- }
450
- } catch(e) {}
451
- }
452
-
453
- let temporalHistory = [];
454
- async function refreshTemporal() {
455
- try {
456
- const res = await fetch('/api/temporal/history');
457
- const data = await res.json();
458
- temporalHistory = data;
459
- renderTemporalTimeline();
460
- } catch(e) {}
461
- }
462
-
463
- async function refreshApprovals() {
464
- try {
465
- const res = await fetch('/api/approvals');
466
- const data = await res.json();
467
- currentApprovals = data;
468
- renderApprovals();
469
- } catch(e) {}
470
- }
471
-
472
- async function refreshMemory() {
473
- try {
474
- const res = await fetch('/api/memory');
475
- const data = await res.json();
476
- document.getElementById('memory-map').innerHTML = `<pre>${escapeHtml(JSON.stringify(data.graph || {}, null, 2))}</pre>`;
477
- document.getElementById('memory-stats-list').innerHTML = `<div class="stat-value">${data.count || 0} <span class="stat-unit">Items Indexed</span></div>`;
478
- } catch(e) {}
479
- }
480
-
481
- async function refreshTeam() {
482
- try {
483
- const res = await fetch('/api/team');
484
- const data = await res.json();
485
- renderTeam(data);
486
- } catch(e) {}
487
- }
488
-
489
- async function decide(id, decision) {
490
- const approval = currentApprovals.find(a => a.id === id);
491
- if (!approval) return;
492
-
493
- if (decision === 'approve' && approval.tier === 3) {
494
- currentTier3Approval = approval;
495
- const expected = `${approval.phase}-${approval.plan}`;
496
- document.getElementById('confirm-title').innerText = 'Critical Tier 3 Approval';
497
- document.getElementById('confirm-desc').innerText = `Danger: You are approving a sensitive change (Phase ${approval.phase}, Plan ${approval.plan}). This requires manual confirmation.`;
498
- document.getElementById('tier3-input-group').style.display = 'block';
499
- document.getElementById('confirm-input').value = '';
500
- document.getElementById('confirm-overlay').style.display = 'flex';
501
- document.getElementById('confirm-btn').onclick = () => submitDecision(id, decision, document.getElementById('confirm-input').value);
502
- return;
503
- }
504
-
505
- submitDecision(id, decision);
506
- }
507
-
508
- async function submitDecision(id, decision, confirmationId = null) {
509
- try {
510
- const res = await fetch(`/api/approve/${id}`, {
511
- method: 'POST',
512
- headers: { 'Content-Type': 'application/json' },
513
- body: JSON.stringify({
514
- decision,
515
- comment: 'Decided via Dashboard',
516
- approver: 'admin-dash',
517
- confirmation_id: confirmationId
518
- })
519
- });
520
- const result = await res.json();
521
- if (!result.success) alert(result.error);
522
- closeConfirm();
523
- refreshApprovals();
524
- } catch(e) { alert('Action failed'); }
525
- }
526
-
527
- function closeConfirm() {
528
- document.getElementById('confirm-overlay').style.display = 'none';
529
- }
530
-
531
- // ── UI Rendering ──────────────────────────────────────────────────────────
532
- function updateStatusUI(data) {
533
- document.getElementById('project-name').textContent = `PROJ: ${data.project_name || 'UNSET'}`;
534
- document.getElementById('stat-phase').textContent = data.phase || '0';
535
- document.getElementById('stat-status').textContent = (data.auto_status || 'IDLE').toUpperCase();
536
- document.getElementById('stat-tasks').textContent = `${data.tasks_completed || 0}/${data.tasks_total || 0}`;
537
- document.getElementById('stat-elapsed').textContent = data.elapsed_ms ? (data.elapsed_ms / 1000).toFixed(1) + 's' : '0s';
538
- }
539
-
540
- function updateMetricsUI(data) {
541
- document.getElementById('stat-total-cost').textContent = `$${(data.costs?.reduce((a,b) => a + b.cost, 0) || 0).toFixed(4)}`;
542
- document.getElementById('stat-avg-quality').textContent = (data.quality?.reduce((a,b) => a + b.score, 0) / (data.quality?.length || 1)).toFixed(1);
543
- }
544
-
545
- function appendAuditEvent(data) {
546
- const div = document.createElement('div');
547
- div.className = `event type-${data.event}`;
548
- const time = new Date(data.timestamp).toLocaleTimeString([], { hour12:false, hour:'2-digit', minute:'2-digit', second:'2-digit' });
549
- 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>`;
550
- feed.prepend(div);
551
- if (feed.children.length > 200) feed.lastChild.remove();
552
- }
553
-
554
- function renderApprovals() {
555
- const list = document.getElementById('approval-list');
556
- if (currentApprovals.length === 0) {
557
- list.innerHTML = '<div style="text-align:center; padding: 40px; color:var(--muted)">No pending approvals</div>';
558
- return;
559
- }
560
-
561
- list.innerHTML = currentApprovals.map(a => `
562
- <div class="card approval-card">
563
- <div class="card-body">
564
- <div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:12px;">
565
- <div>
566
- <span class="tier-badge tier-${a.tier}">Tier ${a.tier}</span>
567
- <span style="font-weight:700">Phase ${a.phase} | ${a.plan}</span>
568
- </div>
569
- <div style="font-size:11px; color:var(--muted)">Expires: ${new Date(a.expires_at).toLocaleTimeString()}</div>
570
- </div>
571
- <p style="margin-bottom:16px; font-size:12px; line-height:1.4">${escapeHtml(a.summary || 'No summary provided')}</p>
572
- <div style="display:flex; gap:12px;">
573
- <button class="btn btn-approve" onclick="decide('${a.id}', 'approve')">Approve Selection</button>
574
- <button class="btn btn-reject" onclick="decide('${a.id}', 'reject')">Reject</button>
575
- </div>
576
- </div>
577
- </div>
578
- `).join('');
579
- }
580
-
581
- function renderTeam(data) {
582
- const list = document.getElementById('team-activity-list');
583
- if (!data || data.length === 0) {
584
- list.innerHTML = '<div style="text-align:center; padding: 40px; color:var(--muted)">No recent team activity</div>';
585
- return;
586
- }
587
- list.innerHTML = data.map(ev => `
588
- <div class="activity-row">
589
- <div class="avatar">${(ev.user || '?').charAt(0).toUpperCase()}</div>
590
- <div class="activity-info">
591
- <div><span class="activity-user">${escapeHtml(ev.user)}</span> <span class="activity-action">${escapeHtml(ev.action)}</span></div>
592
- <div class="activity-time">${new Date(ev.timestamp).toLocaleString()}</div>
593
- </div>
594
- </div>
595
- `).join('');
596
- }
597
-
598
- // ── Simple Charts Implementation ──────────────────────────────────────────
599
- function drawCharts() {
600
- drawCanvasChart('chart-costs', state.costs?.map(c => c.cost) || [], varColor('--accent'));
601
- drawCanvasChart('chart-quality', state.quality?.map(q => q.score) || [], varColor('--green'), 100);
602
- }
603
-
604
- function varColor(name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); }
605
-
606
- function drawCanvasChart(id, data, color, maxVal = null) {
607
- const canvas = document.getElementById(id);
608
- if (!canvas) return;
609
- const ctx = canvas.getContext('2d');
610
- const dpr = window.devicePixelRatio || 1;
611
- canvas.width = canvas.offsetWidth * dpr;
612
- canvas.height = canvas.offsetHeight * dpr;
613
- ctx.scale(dpr, dpr);
614
-
615
- const w = canvas.offsetWidth;
616
- const h = canvas.offsetHeight;
617
- ctx.clearRect(0,0,w,h);
618
-
619
- if (data.length < 2) return;
620
-
621
- const max = maxVal || Math.max(...data) * 1.2 || 1;
622
- const step = w / (data.length - 1);
623
-
624
- ctx.beginPath();
625
- ctx.strokeStyle = color;
626
- ctx.lineWidth = 2;
627
- ctx.lineJoin = 'round';
628
-
629
- data.forEach((val, i) => {
630
- const x = i * step;
631
- const y = h - (val / max) * h;
632
- if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
633
- });
634
- ctx.stroke();
635
-
636
- // Area fill
637
- ctx.lineTo(w, h); ctx.lineTo(0, h); ctx.closePath();
638
- ctx.fillStyle = color.replace(')', ', 0.1)').replace('rgb', 'rgba');
639
- ctx.fill();
640
- }
641
-
642
- // ── Temporal Steering Logic ────────────────────────────────────────────────
643
- function renderTemporalTimeline() {
644
- const slider = document.getElementById('temporal-slider');
645
- const count = document.getElementById('timeline-count');
646
-
647
- if (temporalHistory.length === 0) {
648
- slider.max = 0;
649
- count.textContent = '0 Snapshots';
650
- return;
651
- }
652
-
653
- slider.max = temporalHistory.length - 1;
654
- count.textContent = `${temporalHistory.length} Snapshots`;
655
-
656
- // Default to latest if not touching
657
- if (slider.value == 0 && temporalHistory.length > 0) {
658
- onSliderChange(temporalHistory.length - 1);
659
- slider.value = temporalHistory.length - 1;
660
- }
661
- }
662
-
663
- let selectedSnapshot = null;
664
- async function onSliderChange(index) {
665
- const snap = temporalHistory[temporalHistory.length - 1 - index]; // reversed for chronological range
666
- if (!snap) return;
667
-
668
- selectedSnapshot = snap;
669
- document.getElementById('slider-current').textContent = `Point: ${snap.id.slice(0, 8)} (${new Date(snap.timestamp).toLocaleTimeString()})`;
670
- document.getElementById('inject-btn').disabled = false;
671
-
672
- // Fetch sample audit file for this snapshot
673
- try {
674
- const res = await fetch(`/api/temporal/snapshot/${snap.id}/AUDIT.jsonl`);
675
- const content = await res.text();
676
- document.getElementById('snapshot-viewer').textContent = content || 'No audit log available for this point.';
677
- } catch(e) {
678
- document.getElementById('snapshot-viewer').textContent = 'Failed to load snapshot details.';
679
- }
680
- }
681
-
682
- async function injectHindsight() {
683
- if (!selectedSnapshot) return;
684
- const instruction = document.getElementById('steering-input').value;
685
- if (!instruction) {
686
- alert('Please provide a steering instruction for the hindsight injection.');
687
- return;
688
- }
689
-
690
- const btn = document.getElementById('inject-btn');
691
- btn.disabled = true;
692
- btn.textContent = 'Injecting Steering Vector...';
693
-
694
- try {
695
- const res = await fetch('/api/temporal/inject', {
696
- method: 'POST',
697
- headers: { 'Content-Type': 'application/json' },
698
- body: JSON.stringify({
699
- auditId: selectedSnapshot.id,
700
- fixDescription: instruction
701
- })
702
- });
703
- const result = await res.json();
704
- if (result.success) {
705
- alert('Hindsight Injection Successful. Agent state rolled back and awaiting re-optimization.');
706
- showPage('activity');
707
- } else {
708
- alert('Injection failed: ' + result.error);
709
- }
710
- } catch(e) {
711
- alert('Failed to connect to temporal engine.');
712
- } finally {
713
- btn.disabled = false;
714
- btn.textContent = 'Rewind & Inject Fix';
715
- }
716
- }
717
-
718
- // ── RevOps Logic ─────────────────────────────────────────────────────────
719
- function updateRevOpsUI(data) {
720
- const { roi, velocity, debt } = data;
721
-
722
- document.getElementById('rev-roi').textContent = `${roi.roi_percentage}%`;
723
- document.getElementById('rev-net').textContent = `$${roi.net_value}`;
724
- document.getElementById('rev-velocity').innerHTML = `${velocity.avg_seconds_per_task} <span class="stat-unit">sec/task</span>`;
725
- document.getElementById('rev-eta').textContent = `ETA: ${velocity.eta}`;
726
- document.getElementById('rev-health').textContent = debt.security_health_score;
727
-
728
- const healthStatus = document.getElementById('rev-health-status');
729
- healthStatus.textContent = `STATUS: ${debt.governance_status.toUpperCase()}`;
730
- healthStatus.style.background = debt.security_health_score > 80 ? 'rgba(63, 185, 80, 0.15)' : 'rgba(248, 81, 73, 0.15)';
731
- healthStatus.style.color = debt.security_health_score > 80 ? 'var(--green)' : 'var(--red)';
732
-
733
- document.getElementById('roi-hours').textContent = `${roi.hours_saved}h`;
734
- document.getElementById('roi-gross').textContent = `$${roi.gross_value}`;
735
- document.getElementById('roi-burn').textContent = `$${roi.token_cost}`;
736
- document.getElementById('roi-total-pct').textContent = `${roi.roi_percentage}%`;
737
-
738
- document.getElementById('debt-critical').textContent = debt.critical_findings;
739
- document.getElementById('debt-tier3').textContent = debt.tier3_approvals;
740
- const riskBadge = document.getElementById('debt-risk');
741
- riskBadge.textContent = debt.debt_level.toUpperCase();
742
- riskBadge.className = `badge ${debt.debt_level === 'Minimal' ? 'badge-live' : ''}`;
743
- if (debt.debt_level !== 'Minimal') riskBadge.style.color = 'var(--yellow)';
744
- }
745
-
746
- window.onresize = drawCharts;
747
- setInterval(refreshData, 10000); // More frequent refresh for live dashboard
748
- showPage('activity');
749
- </script>
373
+ <script src="app.js" defer></script>
750
374
  </body>
751
375
  </html>
@@ -7,6 +7,7 @@
7
7
 
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
+ const { ledgerPath, entryCost, entryDay } = require('../models/usage-record');
10
11
 
11
12
  // ── TTL Cache (5-second window) ──────────────────────────────────────────────
12
13
  const _cache = new Map();
@@ -29,7 +30,7 @@ function cacheSet(key, data) {
29
30
  // Paths are resolved lazily to support testing in temp directories
30
31
  const getPaths = () => ({
31
32
  quality: path.join(process.cwd(), '.mindforge', 'metrics', 'session-quality.jsonl'),
32
- usage: path.join(process.cwd(), '.mindforge', 'metrics', 'token-usage.jsonl'),
33
+ usage: ledgerPath(),
33
34
  audit: path.join(process.cwd(), '.planning', 'AUDIT.jsonl'),
34
35
  handoff: path.join(process.cwd(), '.planning', 'HANDOFF.json'),
35
36
  auto: path.join(process.cwd(), '.planning', 'auto-state.json'),
@@ -122,12 +123,29 @@ function getMetrics() {
122
123
  const auditEntries = readJSONL(paths.audit, 500);
123
124
 
124
125
  // Quality scores (last 20 sessions)
126
+ // session-quality.jsonl has NO writer anywhere in this repo — every one of its
127
+ // four references (here, sdk/src/client.ts, mcp-server/src/vendor/client.ts,
128
+ // plugins/mindforge/mcp/dist/index.js) is a READ, and no command instructs an
129
+ // agent to append it either. The read is kept because those three other
130
+ // consumers depend on the file and a schema-compliant one may exist in a user
131
+ // project; supplying a writer is OUT OF SCOPE for this patch. Read the
132
+ // SCHEMA-DECLARED names first (.mindforge/metrics/METRICS-SCHEMA.md), keeping
133
+ // the historical names as fallbacks.
134
+ // Per-session cost is NOT a session-quality field: it is joined from the
135
+ // usage ledger by session_id (every provider result carries one — see
136
+ // bin/models/model-client.js:69).
137
+ const costBySession = usageEntries.reduce((acc, u) => {
138
+ if (!u || typeof u.session_id !== 'string') return acc;
139
+ acc[u.session_id] = (acc[u.session_id] || 0) + entryCost(u);
140
+ return acc;
141
+ }, {});
142
+
125
143
  const sessions = qualityEntries.map(e => ({
126
144
  id: e.session_id,
127
145
  timestamp: e.timestamp,
128
- quality_score: e.quality_score ?? 0,
146
+ quality_score: e.session_quality_score ?? e.quality_score ?? 0,
129
147
  verify_pass_rate: e.verify_pass_rate ?? 0,
130
- cost_usd: e.total_cost_usd ?? 0,
148
+ cost_usd: costBySession[e.session_id] ?? 0,
131
149
  node_repairs: e.node_repairs ?? 0,
132
150
  }));
133
151
 
@@ -165,36 +183,60 @@ function getMetrics() {
165
183
  }
166
184
 
167
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
+ */
168
206
  function getApprovals() {
169
207
  const paths = getPaths();
170
- if (!fs.existsSync(paths.approvals)) return { pending: [], approved: [], rejected: [], expired: [] };
208
+ if (!fs.existsSync(paths.approvals)) return [];
171
209
 
172
- 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();
173
217
  const files = fs.readdirSync(paths.approvals)
174
- .filter(f => f.startsWith('APPROVAL-') && f.endsWith('.json'))
218
+ .filter(f => f.endsWith('.json'))
175
219
  .sort();
176
220
 
177
- const pending = [];
178
- const approved = [];
179
- const rejected = [];
180
- const expired = [];
181
-
221
+ const out = [];
182
222
  for (const f of files) {
183
- try {
184
- const data = JSON.parse(fs.readFileSync(path.join(paths.approvals, f), 'utf8'));
185
- const expiry = data.expires_at ? new Date(data.expires_at).getTime() : Infinity;
186
- const hoursRemaining = expiry === Infinity ? null : (expiry - now) / 3_600_000;
187
-
188
- const enriched = { ...data, hours_remaining: hoursRemaining };
189
-
190
- if (data.status === 'approved') approved.push(enriched);
191
- else if (data.status === 'rejected') rejected.push(enriched);
192
- else if (expiry < now) expired.push({ ...enriched, status: 'expired' });
193
- else pending.push(enriched);
194
- } 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
+ });
195
238
  }
196
-
197
- return { pending, approved, rejected, expired };
239
+ return out;
198
240
  }
199
241
 
200
242
  // ── Team activity ─────────────────────────────────────────────────────────────
@@ -302,12 +344,15 @@ function getCosts(windowDays = 7) {
302
344
  };
303
345
 
304
346
  for (const e of entries) {
305
- if (e.timestamp < cutoff) continue;
306
-
307
- const cost = e.total_cost_usd || 0;
347
+ // Ledger rows carry both `date` and `timestamp` (see bin/models/usage-record.js).
348
+ // Bucket on the canonical day so date-only and full-ISO rows behave alike.
349
+ const day = entryDay(e);
350
+ if (!day || day < cutoff) continue;
351
+
352
+ const cost = entryCost(e);
308
353
  stats.total_usd += cost;
309
-
310
- if (e.timestamp && e.timestamp.startsWith(today)) {
354
+
355
+ if (day === today) {
311
356
  stats.today_usd += cost;
312
357
  }
313
358
 
@@ -10,6 +10,7 @@ const roiEngine = require('../revops/roi-engine');
10
10
  const velocityForecaster = require('../revops/velocity-forecaster');
11
11
  const debtMonitor = require('../revops/debt-monitor');
12
12
  const metricsAggregator = require('./metrics-aggregator');
13
+ const { sendServerError } = require('./error-response');
13
14
 
14
15
  /**
15
16
  * GET /api/revops/overview
@@ -25,7 +26,11 @@ router.get('/overview', (req, res) => {
25
26
  ...metrics,
26
27
  tasks_total: status.tasks_total || 0,
27
28
  tasks_completed: status.tasks_completed || 0,
28
- auditEntries: metricsAggregator.getAuditEntries(500) // need enough history for velocity
29
+ // .entries — getAuditEntries() returns { entries, total, limit, offset }, not an
30
+ // array. roi-engine, velocity-forecaster and debt-monitor all call .filter() on
31
+ // this value, so handing them the wrapper object throws a TypeError that the
32
+ // route's catch turns into an opaque 500.
33
+ auditEntries: metricsAggregator.getAuditEntries(500).entries // need enough history for velocity
29
34
  };
30
35
 
31
36
  const roi = roiEngine.calculate(fullMetrics);
@@ -40,7 +45,12 @@ router.get('/overview', (req, res) => {
40
45
  timestamp: new Date().toISOString()
41
46
  });
42
47
  } catch (err) {
43
- res.status(500).json({ success: false, error: 'AgRevOps metrics retrieval failed', detail: err.message });
48
+ // LEAK-01: echoing err.message as a `detail` field put absolute filesystem paths
49
+ // — and therefore the operator's username and home directory — into an
50
+ // unauthenticated HTTP response body (requireAuth exempts GET). Log the full
51
+ // error server-side; return a generic message plus a correlation id.
52
+ sendServerError(res, 'GET /api/revops/overview', err,
53
+ 'AgRevOps metrics retrieval failed', { success: false });
44
54
  }
45
55
  });
46
56