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.
- package/.agent/CLAUDE.md +37 -13
- package/.agent/hooks/mindforge-block-no-verify.js +61 -13
- package/.agent/hooks/mindforge-config-protection.js +82 -3
- package/.agent/hooks/mindforge-context-monitor.js +1 -1
- package/.agent/hooks/mindforge-workflow-guard.js +2 -2
- package/.agent/hooks/run-with-flags.js +190 -20
- package/.agent/mindforge/browse.md +2 -2
- package/.agent/mindforge/checkpoint.md +1 -1
- package/.agent/mindforge/harness-audit.md +1 -1
- package/.agent/mindforge/orch-add-feature.md +1 -1
- package/.agent/mindforge/orch-build-mvp.md +1 -1
- package/.agent/mindforge/orch-change-feature.md +1 -1
- package/.agent/mindforge/orch-fix-defect.md +1 -1
- package/.agent/mindforge/orch-refine-code.md +1 -1
- package/.agent/mindforge/qa.md +2 -2
- package/.claude/CLAUDE.md +37 -13
- package/.claude/commands/mindforge/browse.md +2 -2
- package/.claude/commands/mindforge/checkpoint.md +1 -1
- package/.claude/commands/mindforge/harness-audit.md +1 -1
- package/.claude/commands/mindforge/orch-add-feature.md +1 -1
- package/.claude/commands/mindforge/orch-build-mvp.md +1 -1
- package/.claude/commands/mindforge/orch-change-feature.md +1 -1
- package/.claude/commands/mindforge/orch-fix-defect.md +1 -1
- package/.claude/commands/mindforge/orch-refine-code.md +1 -1
- package/.claude/commands/mindforge/qa.md +2 -2
- package/.mindforge/MINDFORGE-SCHEMA.json +1 -1
- package/.mindforge/config.json +3 -3
- package/.mindforge/engine/autonomous/headless-adapter.md +2 -2
- package/.mindforge/engine/temporal-protocol.md +2 -2
- package/.mindforge/governance/change-classifier.md +20 -4
- package/.mindforge/memory/sync-manifest.json +1 -1
- package/.mindforge/skills/agent-architecture-audit/SKILL.md +2 -2
- package/.mindforge/skills/orch-pipeline/SKILL.md +4 -4
- package/CHANGELOG.md +194 -0
- package/MINDFORGE.md +13 -6
- package/README.md +4 -3
- package/RELEASENOTES.md +2 -2
- package/SECURITY.md +22 -3
- package/bin/autonomous/auto-runner.js +65 -2
- package/bin/change-classifier.js +151 -16
- package/bin/dashboard/api-router.js +18 -38
- package/bin/dashboard/frontend/app.js +429 -0
- package/bin/dashboard/frontend/index.html +13 -406
- package/bin/dashboard/metrics-aggregator.js +46 -22
- package/bin/dashboard/server.js +160 -1
- package/bin/dashboard/sse-bridge.js +11 -8
- package/bin/engine/sre-manager.js +1 -1
- package/bin/engine/temporal-cli.js +56 -6
- package/bin/engine/verification-runner.js +134 -17
- package/bin/engine/verify-cli.js +25 -7
- package/bin/governance/approval-record.js +147 -0
- package/bin/governance/approve.js +12 -7
- package/bin/governance/policy-engine.js +33 -3
- package/bin/governance/policy-gate-hardened.js +36 -1
- package/bin/governance/verify-approvals.js +163 -0
- package/bin/harness-audit.js +224 -10
- package/bin/hooks/instinct-capture-hook.js +12 -4
- package/bin/install.js +63 -3
- package/bin/installer/harness-adapter-compliance.js +339 -28
- package/bin/installer/hook-registration.js +504 -0
- package/bin/installer-core.js +451 -63
- package/bin/learning/instinct-cli.js +7 -0
- package/bin/memory/vector-hub.js +196 -13
- package/bin/migrations/0.6.0-to-1.0.0.js +30 -25
- package/bin/migrations/1.0.0-to-2.0.0.js +22 -23
- package/bin/mindforge-cli.js +67 -6
- package/bin/models/cost-tracker.js +104 -6
- package/bin/models/model-client.js +6 -1
- package/bin/revops/debt-monitor.js +57 -13
- package/bin/security/trust-gate-hook.js +50 -6
- package/bin/skill-validator.js +6 -1
- package/bin/skills-builder/skill-scorer.js +46 -6
- package/bin/updater/self-update.js +6 -1
- package/bin/updater/version-comparator.js +21 -1
- package/bin/utils/mindforge-version.js +99 -0
- package/bin/utils/redact-secrets.js +106 -0
- package/bin/validate-config.js +42 -2
- package/bin/wizard/setup-wizard.js +4 -1
- package/bin/wizard/theme.js +9 -1
- package/changelogs/index.json +11 -9
- package/changelogs/v11.9.3.md +195 -0
- package/docs/References/config-reference.md +5 -2
- package/docs/References/sdk-api.md +1 -1
- package/docs/Templates/Codebase/architecture.md +1 -1
- package/docs/commands-reference.md +4 -5
- package/docs/faq.md +25 -5
- package/docs/getting-started.md +3 -3
- package/docs/sdk-reference.md +15 -7
- package/docs/troubleshooting.md +10 -6
- package/docs/user-guide.md +14 -14
- package/examples/sdk-integration/README.md +1 -1
- package/package.json +7 -3
- package/subagents/.claude-plugin/marketplace.json +1 -1
- 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
|
|
157
|
+
<div id="connection-status" class="badge">● Connecting…</div>
|
|
158
158
|
</header>
|
|
159
159
|
|
|
160
160
|
<nav>
|
|
161
|
-
<button class="tab active"
|
|
162
|
-
<button class="tab"
|
|
163
|
-
<button class="tab"
|
|
164
|
-
<button class="tab"
|
|
165
|
-
<button class="tab"
|
|
166
|
-
<button class="tab"
|
|
167
|
-
<button class="tab"
|
|
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
|
|
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"
|
|
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;"
|
|
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)"
|
|
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 => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[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
|
|
208
|
+
if (!fs.existsSync(paths.approvals)) return [];
|
|
189
209
|
|
|
190
|
-
const
|
|
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.
|
|
218
|
+
.filter(f => f.endsWith('.json'))
|
|
193
219
|
.sort();
|
|
194
220
|
|
|
195
|
-
const
|
|
196
|
-
const approved = [];
|
|
197
|
-
const rejected = [];
|
|
198
|
-
const expired = [];
|
|
199
|
-
|
|
221
|
+
const out = [];
|
|
200
222
|
for (const f of files) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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 ─────────────────────────────────────────────────────────────
|