@monoes/monomindcli 2.0.0 → 2.0.1

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.
@@ -2663,6 +2663,7 @@ function switchProject(path) {
2663
2663
  // Reset Monograph cache so it reloads for the new project
2664
2664
  _mgLoaded = false;
2665
2665
  _mgGraph = null;
2666
+ window._mgRealTotals = null;
2666
2667
  document.getElementById('sb-proj').textContent = path.split('/').filter(Boolean).pop() || '—';
2667
2668
  document.getElementById('sb-path').textContent = path;
2668
2669
  _showNavProjectCtx(path);
@@ -3174,9 +3175,10 @@ async function loadStatusStrip() {
3174
3175
  const strip = document.getElementById('status-strip');
3175
3176
  if (!strip || !DIR) return;
3176
3177
  try {
3177
- const [statusRes, memRes] = await Promise.allSettled([
3178
+ const [statusRes, memRes, metricsRes] = await Promise.allSettled([
3178
3179
  apiFetch('/api/status?dir=' + enc(DIR)),
3179
3180
  apiFetch('/api/memory/stats?dir=' + enc(DIR)),
3181
+ apiFetch('/api/section?name=metrics&dir=' + enc(DIR)),
3180
3182
  ]);
3181
3183
  const checks = statusRes.status === 'fulfilled'
3182
3184
  ? (Array.isArray(statusRes.value) ? statusRes.value : (statusRes.value?.checks || []))
@@ -3193,15 +3195,27 @@ async function loadStatusStrip() {
3193
3195
  pills.push(`<span class="ss-pill ${cls}">${esc(c.name || c.label || c.key || '?')}</span>`);
3194
3196
  });
3195
3197
 
3196
- // HNSW status
3197
- const hnswOn = mem.hnsw === true || mem.hnswEnabled === true || mem.hnsw_enabled === true;
3198
- pills.push(`<span class="ss-pill ${hnswOn ? 'on' : ''}">HNSW ${hnswOn ? 'ON' : 'OFF'}</span>`);
3199
-
3200
3198
  // Patterns count
3201
3199
  if (mem.patterns != null) {
3202
3200
  pills.push(`<span class="ss-pill">PATTERNS ${Number(mem.patterns).toLocaleString()}</span>`);
3203
3201
  }
3204
3202
 
3203
+ // Worker freshness (metrics.workers = [{name, exists, ageMs}])
3204
+ const metricsSec = metricsRes.status === 'fulfilled'
3205
+ ? (metricsRes.value?.metrics || metricsRes.value || {})
3206
+ : {};
3207
+ const workers = Array.isArray(metricsSec.workers) ? metricsSec.workers : [];
3208
+ const fmtAge = ms => ms < 3600e3 ? Math.round(ms / 60e3) + 'm'
3209
+ : ms < 86400e3 ? Math.round(ms / 3600e3) + 'h'
3210
+ : Math.round(ms / 86400e3) + 'd';
3211
+ workers.forEach(w => {
3212
+ if (!w || !w.name) return;
3213
+ const fresh = w.exists && typeof w.ageMs === 'number' && w.ageMs < 12 * 3600e3;
3214
+ const cls = !w.exists ? '' : (fresh ? 'on' : 'warn');
3215
+ const ageTxt = !w.exists ? 'missing' : (typeof w.ageMs === 'number' ? fmtAge(w.ageMs) : '?');
3216
+ pills.push(`<span class="ss-pill ${cls}" title="worker ${esc(w.name)}: ${esc(ageTxt)}">⚙ ${esc(w.name)}</span>`);
3217
+ });
3218
+
3205
3219
  // Chunks count
3206
3220
  if (mem.chunks != null) {
3207
3221
  pills.push(`<span class="ss-pill">CHUNKS ${Number(mem.chunks).toLocaleString()}</span>`);
@@ -3506,8 +3520,20 @@ async function loadMemUsagePeriod(btn, period) {
3506
3520
  }
3507
3521
 
3508
3522
  const totalCost = typeof s.todayCost === 'number' ? s.todayCost : (typeof s.cost === 'number' ? s.cost : null);
3509
- const cacheEff = s.cacheTokens && s.totalTokensIn
3510
- ? Math.round(s.cacheTokens / s.totalTokensIn * 100) : null;
3523
+ // Cache efficiency = cacheRead / (cacheRead + input), capped at 100%.
3524
+ // (The old cacheTokens/totalTokensIn math exceeded 100% because cache
3525
+ // reads are not included in the input-token total.)
3526
+ const cacheEff = s.cacheTokens
3527
+ ? Math.min(100, Math.round(s.cacheTokens / (s.cacheTokens + (Number(s.totalTokensIn) || 0)) * 100))
3528
+ : null;
3529
+
3530
+ // Projects rows sometimes arrive without a usable name — fall back to
3531
+ // the directory basename, then 'unknown'.
3532
+ const projRows = projects.map(p => {
3533
+ const raw = (p.project && p.project !== '?') ? p.project : (p.name && p.name !== '?' ? p.name : '');
3534
+ const base = String(p.dir || p.path || '').split('/').filter(Boolean).pop() || '';
3535
+ return Object.assign({}, p, { project: raw || base || 'unknown' });
3536
+ });
3511
3537
 
3512
3538
  content.innerHTML = `
3513
3539
  <!-- Overview stats -->
@@ -3536,8 +3562,8 @@ async function loadMemUsagePeriod(btn, period) {
3536
3562
  <div style="margin-bottom:14px">${barChart(mcps, 'count', 'server', 'oklch(70% 0.18 60)', 8)}</div>` : ''}
3537
3563
 
3538
3564
  <!-- Projects -->
3539
- ${projects.length ? `<div class="m-group-title" style="margin-bottom:6px">Projects</div>
3540
- <div style="margin-bottom:14px">${barChart(projects, 'cost', 'project', 'oklch(65% 0.15 150)', 5)}</div>` : ''}
3565
+ ${projRows.length ? `<div class="m-group-title" style="margin-bottom:6px">Projects</div>
3566
+ <div style="margin-bottom:14px">${barChart(projRows, 'cost', 'project', 'oklch(65% 0.15 150)', 5)}</div>` : ''}
3541
3567
 
3542
3568
  <!-- Fallback: session breakdown table if no breakdown data -->
3543
3569
  ${!models.length && !tools.length && rows.length ? `
@@ -6274,7 +6300,6 @@ const _v2AvatarKnown = new Set([
6274
6300
  'quorum-manager','consensus-coordinator','perf-analyzer','benchmarker',
6275
6301
  'task-orchestrator','memory-coordinator','load-balancer','resource-allocator',
6276
6302
  'pr-manager','code-review-swarm','issue-tracker','release-manager','repo-architect',
6277
- 'workflow-automation','sparc-coord','sparc-coder','specification','pseudocode',
6278
6303
  'architecture','refinement','backend-dev','frontend-developer','mobile-dev',
6279
6304
  'ml-developer','cicd-engineer','system-architect','ai-engineer','model-qa',
6280
6305
  'data-engineer','analytics-reporter','experiment-tracker','data-consolidator',
@@ -8227,7 +8252,7 @@ function v2RenderOrgConfig() {
8227
8252
  const rc = d.run_config || {};
8228
8253
  const gov = (d.governance && typeof d.governance === 'object') ? d.governance : { policy: d.governance || 'auto' };
8229
8254
  const topos = ['hierarchical','hierarchical-mesh','mesh','star','ring','adaptive','hybrid'];
8230
- const modes = ['daemon','once','scheduled'];
8255
+ const modes = [{ v: 'daemon', l: 'persistent (daemon)' }, { v: 'once', l: 'once' }, { v: 'scheduled', l: 'scheduled' }];
8231
8256
  const statuses = ['active','paused','archived'];
8232
8257
  const govPolicies = ['auto','board','strict'];
8233
8258
  const inp = (id,val,type,extra) => '<input class="filter-input" id="'+id+'" type="'+(type||'text')+'" value="'+esc(String(val??''))+'" '+(extra||'')+' style="width:100%;box-sizing:border-box">';
@@ -8239,7 +8264,7 @@ function v2RenderOrgConfig() {
8239
8264
  fld('Name', inp('oc-name', d.name||'','text','readonly style="opacity:0.5"'))
8240
8265
  +fld('Status', sel('oc-status', statuses, d.status||'active'))
8241
8266
  +'<div style="grid-column:1/-1">'+fld('Goal', '<textarea id="oc-goal" class="filter-input" rows="3" style="width:100%;box-sizing:border-box;resize:vertical;line-height:1.5">'+esc(d.goal||'')+'</textarea>')+'</div>'
8242
- +fld('Mode', sel('oc-mode', modes, d.mode||'daemon'))
8267
+ +fld('Mode', '<select class="filter-input" id="oc-mode" style="width:100%;box-sizing:border-box;cursor:pointer">'+modes.map(o=>'<option value="'+esc(o.v)+'"'+((d.mode||'daemon')===o.v?' selected':'')+'>'+esc(o.l)+'</option>').join('')+'</select>')
8243
8268
  +fld('Topology', sel('oc-topology', topos, d.topology||'hierarchical'))
8244
8269
  +'<div style="grid-column:1/-1">'+fld('Schedule', inp('oc-schedule', d.schedule||''))+'</div>'
8245
8270
  )
@@ -9144,7 +9169,6 @@ const _MASTERMIND_SKILLS = [
9144
9169
  '/monomind:repeat','monomind:review','monomind:understand','monomind:adr',
9145
9170
  '/monomind:budget','monomind:graph-status','monomind:loops','monomind:swarm',
9146
9171
  '/swarm:development','swarm:analysis','swarm:testing','swarm:optimization',
9147
- '/sparc:architect','sparc:code','sparc:tdd','sparc:reviewer','sparc:security-review',
9148
9172
  '/github:pr-manager','github:issue-tracker','github:release-manager',
9149
9173
  ];
9150
9174
  window._allSkills = _MASTERMIND_SKILLS;
@@ -11871,8 +11895,16 @@ function renderMgOverview() {
11871
11895
  const E = g.edges.length;
11872
11896
  const avgDeg = N > 0 ? ((2 * E) / N).toFixed(1) : '0';
11873
11897
  const typeSet = new Set(g.nodes.map(n => n.type || n.kind || 'unknown'));
11874
- document.getElementById('mg-stat-nodes').textContent = N;
11875
- document.getElementById('mg-stat-edges').textContent = E;
11898
+ // The graph fetch is capped at 2000 nodes prefer real totals from the
11899
+ // monograph report (set by mgLoadReport) and relabel honestly when capped.
11900
+ const _capped = N >= 2000 || E >= 2000;
11901
+ const _rt = window._mgRealTotals || null;
11902
+ const _nEl = document.getElementById('mg-stat-nodes');
11903
+ const _eEl = document.getElementById('mg-stat-edges');
11904
+ _nEl.textContent = (_rt && _rt.nodes) ? Number(_rt.nodes).toLocaleString() : N;
11905
+ _eEl.textContent = (_rt && _rt.edges) ? Number(_rt.edges).toLocaleString() : E;
11906
+ if (_nEl.previousElementSibling) _nEl.previousElementSibling.textContent = ((_rt && _rt.nodes) || !_capped) ? 'Nodes' : 'Nodes (top 2000)';
11907
+ if (_eEl.previousElementSibling) _eEl.previousElementSibling.textContent = ((_rt && _rt.edges) || !_capped) ? 'Edges' : 'Edges (top 2000)';
11876
11908
  document.getElementById('mg-stat-avgdeg').textContent = avgDeg;
11877
11909
  document.getElementById('mg-stat-types').textContent = typeSet.size;
11878
11910
 
@@ -11918,7 +11950,14 @@ function renderMgOverview() {
11918
11950
  else {
11919
11951
  const show = dead.slice(0, 12);
11920
11952
  deadEl.innerHTML = `<table class="mg-table"><thead><tr><th>Name</th><th>Type</th></tr></thead><tbody>` +
11921
- show.map(n => `<tr><td title="${esc(n.id||n.name||n.label)}">${esc(mgShortLabel(n.label||n.name||n.id||''))}</td><td>${esc(n.type||n.kind||'')}</td></tr>`).join('') +
11953
+ show.map(n => {
11954
+ // Prefer a real name; if the label is just a generic type word
11955
+ // (e.g. "function"), fall back to the qualified id/path instead.
11956
+ const raw = n.label || n.name || '';
11957
+ const generic = !raw || /^(function|method|class|node|unknown|anonymous)$/i.test(String(raw).trim());
11958
+ const display = generic ? (n.id || n.name || raw || '') : raw;
11959
+ return `<tr><td title="${esc(n.id||n.name||n.label)}">${esc(mgShortLabel(display))}</td><td>${esc(n.type||n.kind||'')}</td></tr>`;
11960
+ }).join('') +
11922
11961
  (dead.length > 12 ? `<tr><td colspan="2" style="color:var(--text-xs);font-size:11px">…and ${dead.length-12} more</td></tr>` : '') +
11923
11962
  `</tbody></table>`;
11924
11963
  }
@@ -12820,6 +12859,18 @@ async function mgLoadReport() {
12820
12859
  // Extract markdown from JSON envelope {exists, report, stats}
12821
12860
  const markdown = (data && data.report) ? data.report : (typeof data === 'string' ? data : JSON.stringify(data, null, 2));
12822
12861
  const stats = (data && data.stats) || {};
12862
+ // Real graph totals — the overview cards otherwise show the capped fetch size
12863
+ const _mdN = (String(markdown).match(/\*\*Nodes\*\*:\s*([\d,]+)/i) || [])[1];
12864
+ const _mdE = (String(markdown).match(/\*\*Edges\*\*:\s*([\d,]+)/i) || [])[1];
12865
+ const _totN = stats.nodes || (_mdN ? Number(_mdN.replace(/,/g, '')) : null);
12866
+ const _totE = stats.edges || (_mdE ? Number(_mdE.replace(/,/g, '')) : null);
12867
+ if (_totN || _totE) {
12868
+ window._mgRealTotals = { nodes: _totN, edges: _totE };
12869
+ const nEl = document.getElementById('mg-stat-nodes');
12870
+ const eEl = document.getElementById('mg-stat-edges');
12871
+ if (_totN && nEl) { nEl.textContent = Number(_totN).toLocaleString(); if (nEl.previousElementSibling) nEl.previousElementSibling.textContent = 'Nodes'; }
12872
+ if (_totE && eEl) { eEl.textContent = Number(_totE).toLocaleString(); if (eEl.previousElementSibling) eEl.previousElementSibling.textContent = 'Edges'; }
12873
+ }
12823
12874
  mgRenderReport(markdown, el, stats);
12824
12875
  } catch (err) {
12825
12876
  el.innerHTML = `<div style="color:var(--red);font-size:12px">Error: ${esc(String(err))}</div>`;
@@ -13156,7 +13207,7 @@ function mgWikiFindRelated(nodeId) {
13156
13207
  }
13157
13208
 
13158
13209
  function mgWikiRefresh() {
13159
- _mgLoaded = false; _mgGraph = null;
13210
+ _mgLoaded = false; _mgGraph = null; window._mgRealTotals = null;
13160
13211
  document.getElementById('mg-wiki-list').innerHTML = '<div class="loading-txt">Loading…</div>';
13161
13212
  loadMonograph();
13162
13213
  }
@@ -13383,7 +13434,7 @@ async function selectSwarmRun(idx) {
13383
13434
  detail.innerHTML =
13384
13435
  '<div style="margin-bottom:10px">' +
13385
13436
  '<div style="font-size:13px;font-weight:600;color:var(--text-hi)">' + esc((run.swarmId || run.id || '—').toString().slice(0, 14)) + '</div>' +
13386
- '<div style="font-size:11px;color:var(--text-lo);margin-top:3px">' + esc(run.topology || '—') + ' · ' + esc(run.consensus || '—') + ' · ' + (run.agentCount || 0) + ' agents</div>' +
13437
+ '<div style="font-size:11px;color:var(--text-lo);margin-top:3px">' + esc(run.topology || '—') + ' · ' + (run.consensus ? esc(run.consensus) + ' vote-threshold' : '—') + ' · ' + (run.agentCount || 0) + ' agents</div>' +
13387
13438
  '</div>' +
13388
13439
  '<canvas id="swarm-topo-canvas" style="width:100%;max-width:380px;height:190px;display:block;border:1px solid var(--border);border-radius:6px;margin-bottom:12px"></canvas>' +
13389
13440
  '<div id="swarm-agent-list" style="margin-bottom:10px"></div>' +
@@ -13545,25 +13596,40 @@ function _renderChunks(list) {
13545
13596
  grid.innerHTML = '<div class="empty">No chunks indexed.<br><span style="font-size:11px;color:var(--text-xs)">Run /monomind:understand to build the index.</span></div>';
13546
13597
  return;
13547
13598
  }
13548
- grid.innerHTML = list.slice(0, 200).map(c => {
13599
+ // Keep the rendered slice around so button handlers can look chunks up by
13600
+ // index — interpolating chunk content into onclick attributes broke (and
13601
+ // leaked markup) whenever the text contained quotes.
13602
+ const shown = list.slice(0, 200);
13603
+ window._chunksShown = shown;
13604
+ grid.innerHTML = shown.map((c, i) => {
13549
13605
  const src = (c.source || c.file || '').split('/').slice(-2).join('/');
13550
13606
  const excerpt = (c.content || c.text || c.body || '').slice(0, 220);
13551
13607
  const ns = c.namespace || c.type || '';
13552
- const chunkId = JSON.stringify(c.id || c.path || c.source || '');
13553
- const chunkContent = JSON.stringify(c.content || c.text || c.body || '');
13554
- const chunkSrc = JSON.stringify(src);
13555
13608
  return '<div class="chunk-card" data-search="' + esc((src + ' ' + excerpt + ' ' + ns).toLowerCase()) + '">' +
13556
13609
  '<div class="chunk-src">' + esc(src || '—') + '</div>' +
13557
13610
  '<div class="chunk-excerpt">' + esc(excerpt) + '</div>' +
13558
13611
  '<div class="chunk-footer">' +
13559
13612
  (ns ? '<span class="chunk-ns">' + esc(ns) + '</span>' : '') +
13560
- '<button class="btn" style="margin-left:auto;font-size:10px;padding:1px 7px" onclick="openChunkEdit(' + chunkId + ',' + chunkContent + ',' + chunkSrc + ')">✎ Edit</button>' +
13561
- '<button class="btn" style="font-size:10px;padding:1px 7px;color:var(--red);border-color:var(--red)" onclick="deleteChunk(' + chunkId + ')">✕</button>' +
13613
+ '<button class="btn" style="margin-left:auto;font-size:10px;padding:1px 7px" onclick="openChunkEditAt(' + i + ')">✎ Edit</button>' +
13614
+ '<button class="btn" style="font-size:10px;padding:1px 7px;color:var(--red);border-color:var(--red)" onclick="deleteChunkAt(' + i + ')">✕</button>' +
13562
13615
  '</div>' +
13563
13616
  '</div>';
13564
13617
  }).join('');
13565
13618
  }
13566
13619
 
13620
+ function openChunkEditAt(i) {
13621
+ const c = (window._chunksShown || [])[i];
13622
+ if (!c) return;
13623
+ const src = (c.source || c.file || '').split('/').slice(-2).join('/');
13624
+ openChunkEdit(c.id || c.path || c.source || '', c.content || c.text || c.body || '', src);
13625
+ }
13626
+
13627
+ function deleteChunkAt(i) {
13628
+ const c = (window._chunksShown || [])[i];
13629
+ if (!c) return;
13630
+ deleteChunk(c.id || c.path || c.source || '');
13631
+ }
13632
+
13567
13633
  function openChunkEdit(id, content, srcLabel) {
13568
13634
  _editChunkId = id;
13569
13635
  document.getElementById('chunk-modal-title').textContent = 'Edit Chunk';
@@ -13665,7 +13731,28 @@ async function loadAgentGraphTab() {
13665
13731
  if (bar) bar.innerHTML = '<div class="loading-txt">Loading…</div>';
13666
13732
  try {
13667
13733
  const data = await apiFetch('/api/graph?dir=' + enc(DIR));
13668
- _agData = data;
13734
+ // /api/graph returns {nodes, edges} — session nodes + agenttype nodes.
13735
+ // Transform into the summary/sessions shape this tab renders.
13736
+ const nodes = Array.isArray(data && data.nodes) ? data.nodes : [];
13737
+ const sessNodes = nodes.filter(n => n.type === 'session').sort((a, b) => (b.mtime || 0) - (a.mtime || 0));
13738
+ const agNodes = nodes.filter(n => n.type === 'agenttype');
13739
+ const sessions = sessNodes.map(n => ({
13740
+ id: n.id,
13741
+ turns: n.turns || 0,
13742
+ spawnCount: Object.values(n.agentSpawns || {}).reduce((a, b) => a + b, 0),
13743
+ toolCount: n.totalTools || 0,
13744
+ tools: n.toolCounts || {},
13745
+ agentTypes: n.agentSpawns || {},
13746
+ cost: n.cost,
13747
+ }));
13748
+ _agData = {
13749
+ sessions,
13750
+ sessionCount: sessions.length,
13751
+ agentTypes: agNodes.length,
13752
+ totalSpawns: agNodes.reduce((a, n) => a + (n.totalSpawns || 0), 0),
13753
+ totalToolCalls: sessions.reduce((a, s) => a + (s.toolCount || 0), 0),
13754
+ totalCost: sessions.reduce((a, s) => a + (Number(s.cost) || 0), 0),
13755
+ };
13669
13756
  _renderAgSummary();
13670
13757
  _renderAgSessList();
13671
13758
  } catch (e) {
@@ -841,6 +841,15 @@ html, body {
841
841
  <div class="meta-cell-value" id="meta-created" style="font-size:10px;color:var(--muted)">—</div>
842
842
  </div>
843
843
  </div>
844
+
845
+ <!-- Audit Coverage card (populated from /api/org-coverage; hidden when no coverage.json) -->
846
+ <div id="audit-coverage-card" style="display:none; background:var(--bg-panel); border:1px solid var(--border); border-radius:3px; padding:10px 14px;">
847
+ <div class="meta-cell-label" style="display:flex; align-items:center; gap:8px;">
848
+ AUDIT COVERAGE
849
+ <span id="acov-summary" style="letter-spacing:1px; color:var(--muted)"></span>
850
+ </div>
851
+ <div id="acov-areas" style="display:flex; flex-wrap:wrap; gap:6px; margin-top:2px;"></div>
852
+ </div>
844
853
  <div id="chart-svg-wrap">
845
854
  <svg id="org-chart-svg" viewBox="0 0 720 320">
846
855
  <defs>
@@ -1203,6 +1212,51 @@ async function selectOrg(name) {
1203
1212
  }
1204
1213
 
1205
1214
  renderTab(currentTab);
1215
+ loadAuditCoverage(listOrg);
1216
+ }
1217
+
1218
+ // ── Audit Coverage card (reads .monomind/audit/coverage.json via /api/org-coverage) ──
1219
+ async function loadAuditCoverage(listOrg) {
1220
+ const card = document.getElementById('audit-coverage-card');
1221
+ card.style.display = 'none';
1222
+ const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
1223
+ try {
1224
+ const dir = listOrg.projectDir || window._orgDir;
1225
+ const url = '/api/org-coverage' + (dir ? `?dir=${encodeURIComponent(dir)}` : '');
1226
+ const r = await fetch(url);
1227
+ if (!r.ok) return; // 404 = no coverage.json — keep card hidden
1228
+ const cov = await r.json();
1229
+ const areas = cov.areas && typeof cov.areas === 'object' ? cov.areas : {};
1230
+ const names = Object.keys(areas);
1231
+ if (!names.length) return;
1232
+
1233
+ const summ = [];
1234
+ if (cov.areas_converged != null && cov.areas_total != null) summ.push(`${cov.areas_converged}/${cov.areas_total} CONVERGED`);
1235
+ if (cov.current_area) summ.push(`NOW: ${String(cov.current_area).toUpperCase()}`);
1236
+ document.getElementById('acov-summary').textContent = summ.join(' · ');
1237
+
1238
+ const verdictColor = v => {
1239
+ const s = String(v || '').toUpperCase();
1240
+ if (s.includes('CONVERGE') || s.includes('KEEP') || s.includes('PASS') || s.includes('DONE')) return 'var(--green, oklch(68% 0.2 150))';
1241
+ if (s.includes('ITERATE') || s.includes('WARN')) return 'oklch(78% 0.18 80)';
1242
+ if (s.includes('FAIL') || s.includes('REJECT')) return 'var(--red, oklch(62% 0.22 25))';
1243
+ return 'var(--muted, oklch(55% 0.02 186))';
1244
+ };
1245
+ document.getElementById('acov-areas').innerHTML = names.map(name => {
1246
+ const a = areas[name] || {};
1247
+ const verdict = a.verdict || a.status || '—';
1248
+ const value = a.value_delivered || a.valueDelivered || null;
1249
+ const c = verdictColor(verdict);
1250
+ return `<span style="display:inline-flex; align-items:center; gap:5px; font-size:9px; font-family:var(--mono);
1251
+ border:1px solid var(--border); border-radius:3px; padding:2px 7px; color:var(--text);"
1252
+ title="${esc(value || verdict)}">
1253
+ ${esc(name)}
1254
+ <span style="color:${c}; letter-spacing:1px;">${esc(String(verdict).toUpperCase())}</span>
1255
+ ${value ? `<span style="color:var(--dim); max-width:180px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${esc(value)}</span>` : ''}
1256
+ </span>`;
1257
+ }).join('');
1258
+ card.style.display = '';
1259
+ } catch (_) { /* keep hidden */ }
1206
1260
  }
1207
1261
 
1208
1262
  function updateHeader(listOrg, data) {
@@ -171,11 +171,12 @@ function pathToSections(filename) {
171
171
  if (f.includes('registry') || f.includes('registrations')) return ['agents'];
172
172
  if (f.includes('route') || f.includes('worker-dispatch')) return ['hooks'];
173
173
  if (f.includes('chunk') || f.includes('skills')) return ['knowledge'];
174
- if (f.includes('memory.db') || f.includes('memory.graph') || f.includes('hnsw.index') ||
175
- f.includes('monovector.db') || f.includes('ranked-context') ||
174
+ if (f.includes('auto-memory-store') || f.includes('episodes.jsonl') ||
176
175
  (f.includes('/memory/') && f.endsWith('.md'))) return ['memory', 'sessions'];
177
176
  if (f.includes('palace') || f.includes('drawers') || f.includes('identity')) return ['memory', 'sessions'];
178
- if (f.includes('ddd') || f.includes('learning') || f.includes('audit')) return ['metrics'];
177
+ if (f.includes('consolidation')) return ['metrics', 'memory'];
178
+ if (f.includes('ddd') || f.includes('audit') || f.includes('codebase-map') ||
179
+ f.includes('security-audit') || f.includes('performance')) return ['metrics'];
179
180
  if (f.endsWith('.jsonl') || f.includes('sessions')) return ['sessions'];
180
181
  return ['sessions', 'swarm', 'agents', 'tokens', 'hooks'];
181
182
  }
@@ -1403,7 +1404,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1403
1404
  const adrs = [];
1404
1405
  for (const { path: adrDir, group } of adrDirs) {
1405
1406
  if (!fs.existsSync(adrDir)) continue;
1406
- const files = fs.readdirSync(adrDir).filter(f => f.endsWith('.md') && f !== 'README.md' && f !== 'v3-adrs.md' && f !== 'SECURITY-REVIEW-SUMMARY.md');
1407
+ // Skip AppleDouble junk ('._*') exFAT volumes litter these and they aren't real ADRs
1408
+ const files = fs.readdirSync(adrDir).filter(f => f.endsWith('.md') && !f.startsWith('._') && f !== 'README.md' && f !== 'v3-adrs.md' && f !== 'SECURITY-REVIEW-SUMMARY.md');
1407
1409
  for (const fname of files.sort()) {
1408
1410
  const resolvedGroup = /^ADR-G/i.test(fname) ? 'guidance' : 'implementation';
1409
1411
  try {
@@ -1621,10 +1623,26 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1621
1623
  });
1622
1624
  }
1623
1625
 
1624
- // Check for AgentDB / HNSW / RVF backends
1625
- const dbPath = path.join(d, '.monomind', 'agentdb.db');
1626
- const hnswPath = path.join(d, '.monomind', 'hnsw.index');
1627
- const rvfPath = path.join(d, '.monomind', 'memory.rvf');
1626
+ // Real v2 memory sources: auto-memory pattern store + episodic log
1627
+ let patterns = 0, patternsUpdated = null;
1628
+ try {
1629
+ const store = JSON.parse(fs.readFileSync(path.join(d, '.monomind', 'data', 'auto-memory-store.json'), 'utf8'));
1630
+ if (Array.isArray(store)) {
1631
+ patterns = store.length;
1632
+ for (const e of store) {
1633
+ if (e && typeof e.ts === 'number' && (!patternsUpdated || e.ts > patternsUpdated)) patternsUpdated = e.ts;
1634
+ }
1635
+ }
1636
+ } catch {}
1637
+
1638
+ let episodes = 0, lastEpisode = null;
1639
+ try {
1640
+ const lines = fs.readFileSync(path.join(d, '.monomind', 'episodic', 'episodes.jsonl'), 'utf8').split('\n').filter(Boolean);
1641
+ episodes = lines.length;
1642
+ if (lines.length) {
1643
+ try { const last = JSON.parse(lines[lines.length - 1]); lastEpisode = last.ts || last.timestamp || null; } catch {}
1644
+ }
1645
+ } catch {}
1628
1646
 
1629
1647
  const stats = {
1630
1648
  total,
@@ -1633,9 +1651,15 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1633
1651
  ns: Object.keys(byType).length,
1634
1652
  size,
1635
1653
  byType,
1636
- hnsw: fs.existsSync(hnswPath),
1637
- agentdb: fs.existsSync(dbPath),
1638
- rvf: fs.existsSync(rvfPath),
1654
+ patterns,
1655
+ patternsUpdated,
1656
+ episodes,
1657
+ lastEpisode,
1658
+ memoryFiles: total,
1659
+ // Legacy keys (v1 backends removed in v2) — kept false for frontend backward-safety
1660
+ hnsw: false,
1661
+ agentdb: false,
1662
+ rvf: false,
1639
1663
  lastWrite,
1640
1664
  memDir,
1641
1665
  };
@@ -3476,7 +3500,9 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
3476
3500
  if (req.method === 'GET' && url.startsWith('/api/token-usage')) {
3477
3501
  try {
3478
3502
  const qs = new URL(req.url, 'http://localhost').searchParams;
3479
- const period = ['today','week','30days','month'].includes(qs.get('period')) ? qs.get('period') : 'today';
3503
+ // Frontend sends ?range=..., older callers use ?period=... accept both
3504
+ const _periodRaw = qs.get('period') || qs.get('range');
3505
+ const period = ['today','week','30days','month'].includes(_periodRaw) ? _periodRaw : 'today';
3480
3506
  const dir = path.resolve(qs.get('dir') || projectDir || process.cwd());
3481
3507
  const trackerPath = path.join(dir, '.claude', 'helpers', 'token-tracker.cjs');
3482
3508
  const fallback = () => {
@@ -5625,6 +5651,33 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5625
5651
  return;
5626
5652
  }
5627
5653
 
5654
+ // ------------------------------------------------- GET /api/playbooks
5655
+ // List playbook definitions from <dir>/.monomind/playbooks/*.json.
5656
+ // (Previously only POST was registered, so GET /api/playbooks?dir=... fell
5657
+ // through to the 404 handler.)
5658
+ if (req.method === 'GET' && url === '/api/playbooks') {
5659
+ try {
5660
+ const qp = new URL(req.url, 'http://localhost').searchParams;
5661
+ const dir = qp.get('dir') || projectDir || process.cwd();
5662
+ const playbookDir = path.join(path.resolve(dir), '.monomind', 'playbooks');
5663
+ const result = [];
5664
+ if (fs.existsSync(playbookDir)) {
5665
+ const files = fs.readdirSync(playbookDir).filter(f => f.endsWith('.json') && !f.startsWith('._'));
5666
+ for (const file of files) {
5667
+ try {
5668
+ const fpath = path.join(playbookDir, file);
5669
+ const def = JSON.parse(fs.readFileSync(fpath, 'utf8'));
5670
+ const stat = fs.statSync(fpath);
5671
+ result.push({ ...def, id: def.id || file.replace('.json', ''), file, modifiedAt: stat.mtimeMs });
5672
+ } catch (_) {}
5673
+ }
5674
+ }
5675
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
5676
+ res.end(JSON.stringify(result));
5677
+ } catch (e) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message })); }
5678
+ return;
5679
+ }
5680
+
5628
5681
  // ------------------------------------------------- POST /api/playbooks
5629
5682
  // Save a playbook definition to .monomind/playbooks/<id>.json
5630
5683
  if (req.method === 'POST' && url === '/api/playbooks') {
@@ -5685,6 +5738,28 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5685
5738
  return;
5686
5739
  }
5687
5740
 
5741
+ // ------------------------------------------------- GET /api/org-coverage
5742
+ // Audit coverage report written by the audit loop to .monomind/audit/coverage.json
5743
+ if (req.method === 'GET' && url === '/api/org-coverage') {
5744
+ try {
5745
+ const qp = new URL(req.url, 'http://localhost').searchParams;
5746
+ const dir = path.resolve(qp.get('dir') || projectDir || process.cwd());
5747
+ const covPath = path.join(dir, '.monomind', 'audit', 'coverage.json');
5748
+ if (!fs.existsSync(covPath)) {
5749
+ res.writeHead(404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
5750
+ res.end('{}');
5751
+ return;
5752
+ }
5753
+ const coverage = JSON.parse(fs.readFileSync(covPath, 'utf8'));
5754
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
5755
+ res.end(JSON.stringify(coverage));
5756
+ } catch (e) {
5757
+ res.writeHead(500, { 'Content-Type': 'application/json' });
5758
+ res.end(JSON.stringify({ error: e.message }));
5759
+ }
5760
+ return;
5761
+ }
5762
+
5688
5763
  // ------------------------------------------------- GET /api/workflow-runs
5689
5764
  if (req.method === 'GET' && url === '/api/workflow-runs') {
5690
5765
  // Reads from ~/.monomind/browse-runs.json written by the monobrowse dashboard server.