monomind 1.10.6 → 1.10.8

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.
@@ -296,57 +296,82 @@ function _autoIndexKnowledge(knowledgeDir) {
296
296
  } catch (e) {}
297
297
  }
298
298
 
299
- // Inject monograph graph summary as a knowledge chunk
299
+ // Inject monograph graph summary as a knowledge chunk.
300
+ // Reads from .monomind/monograph.db (SQLite, source of truth) and falls
301
+ // back to the legacy .monomind/graph/{stats,graph}.json pair only when
302
+ // present (older installs).
300
303
  try {
301
- var statsPath = path.join(CWD, '.monomind', 'graph', 'stats.json');
302
- var graphPath = path.join(CWD, '.monomind', 'graph', 'graph.json');
303
- if (fs.existsSync(statsPath) && fs.existsSync(graphPath)) {
304
- var stats = JSON.parse(fs.readFileSync(statsPath, 'utf-8'));
305
- var graphStat = fs.statSync(graphPath);
306
- // Only read graph if under 10MB
307
- if (graphStat.size < 10 * 1024 * 1024) {
308
- var graph = JSON.parse(fs.readFileSync(graphPath, 'utf-8'));
309
- var rawNodes = Array.isArray(graph.nodes) ? graph.nodes : [];
310
- var rawEdges = Array.isArray(graph.edges) ? graph.edges : (Array.isArray(graph.links) ? graph.links : []);
311
- // Top 15 nodes by degree (god nodes)
312
- var degree = {};
313
- rawNodes.forEach(function(n) { degree[n.id] = 0; });
314
- rawEdges.forEach(function(e) {
315
- if (degree[e.source] !== undefined) degree[e.source]++;
316
- if (degree[e.target] !== undefined) degree[e.target]++;
317
- });
318
- var topNodes = rawNodes
319
- .filter(function(n) { return n.sourceFile && n.sourceFile !== ''; })
320
- .sort(function(a, b) { return (degree[b.id] || 0) - (degree[a.id] || 0); })
321
- .slice(0, 15);
322
- // Community summary group by type
323
- var typeCounts = {};
324
- rawNodes.forEach(function(n) { var t = n.type || n.label || 'unknown'; typeCounts[t] = (typeCounts[t] || 0) + 1; });
325
- var topTypes = Object.entries(typeCounts).sort(function(a,b){return b[1]-a[1];}).slice(0,8).map(function(e){return e[0]+':'+e[1];}).join(', ');
326
- var builtDate = stats.builtAt ? new Date(stats.builtAt).toISOString().slice(0,10) : 'unknown';
327
- var summaryText = [
328
- 'MONOGRAPH KNOWLEDGE GRAPH SUMMARY',
329
- 'Built: ' + builtDate + ' | Nodes: ' + (stats.nodes || rawNodes.length) + ' | Edges: ' + (stats.edges || rawEdges.length) + ' | Files: ' + (stats.files || 0),
330
- '',
331
- 'TOP GOD NODES (highest connectivity):',
332
- topNodes.map(function(n) {
333
- return ' ' + (n.name || n.id) + ' [' + (n.type || n.label || '?') + '] — ' + (n.sourceFile || '') + ' (degree: ' + (degree[n.id] || 0) + ')';
334
- }).join('\n'),
335
- '',
336
- 'NODE TYPE DISTRIBUTION: ' + topTypes,
337
- '',
338
- 'Use mcp__monomind__monograph_suggest to find files relevant to your task.',
339
- 'Use mcp__monomind__monograph_query to search for symbols.',
340
- 'Use mcp__monomind__monograph_god_nodes for architecture overview.',
341
- ].join('\n');
342
- var chunkId = crypto.createHash('md5').update('monograph-graph-summary').digest('hex').slice(0, 16);
343
- newLines.push(JSON.stringify({
344
- chunkId: chunkId,
345
- namespace: 'knowledge:shared',
346
- text: summaryText,
347
- metadata: { label: 'monograph-graph-summary', builtAt: stats.builtAt }
348
- }));
349
- }
304
+ var mgDbPath2 = path.join(CWD, '.monomind', 'monograph.db');
305
+ var legacyStats2 = path.join(CWD, '.monomind', 'graph', 'stats.json');
306
+ var legacyGraph2 = path.join(CWD, '.monomind', 'graph', 'graph.json');
307
+
308
+ var summaryText = null;
309
+ var summaryMeta = {};
310
+
311
+ if (fs.existsSync(mgDbPath2)) {
312
+ try {
313
+ var mgMod2 = _requireMonograph();
314
+ if (mgMod2 && mgMod2.openDb) {
315
+ var sumDb = mgMod2.openDb(mgDbPath2);
316
+ try {
317
+ var nodeC = sumDb.prepare('SELECT COUNT(*) AS c FROM nodes').get().c;
318
+ var edgeC = sumDb.prepare('SELECT COUNT(*) AS c FROM edges').get().c;
319
+ var topNodes2 = sumDb.prepare(
320
+ 'SELECT n.name, n.label, n.file_path, ' +
321
+ '(SELECT COUNT(*) FROM edges WHERE source_id=n.id OR target_id=n.id) AS deg ' +
322
+ 'FROM nodes n WHERE n.file_path IS NOT NULL AND n.file_path != "" ORDER BY deg DESC LIMIT 15'
323
+ ).all();
324
+ var typeRows = sumDb.prepare(
325
+ 'SELECT label, COUNT(*) AS c FROM nodes GROUP BY label ORDER BY c DESC LIMIT 8'
326
+ ).all();
327
+ var typeStr = typeRows.map(function(r) { return r.label + ':' + r.c; }).join(', ');
328
+ summaryText = [
329
+ 'MONOGRAPH KNOWLEDGE GRAPH SUMMARY',
330
+ 'Source: monograph.db | Nodes: ' + nodeC + ' | Edges: ' + edgeC,
331
+ '',
332
+ 'TOP GOD NODES (highest connectivity start exploration here):',
333
+ topNodes2.map(function(n) {
334
+ return ' ' + n.name + ' [' + n.label + '] — ' + (n.file_path || '') + ' (degree: ' + n.deg + ')';
335
+ }).join('\n'),
336
+ '',
337
+ 'NODE TYPE DISTRIBUTION: ' + typeStr,
338
+ '',
339
+ 'Before grepping or globbing, prefer:',
340
+ ' mcp__monomind__monograph_suggest({ task: "<your task>" }) — ranked relevant files',
341
+ ' mcp__monomind__monograph_query({ q: "<symbol|keyword>" }) BM25 search with file:line',
342
+ ' mcp__monomind__monograph_impact({ name: "<file>" }) upstream + downstream blast radius',
343
+ ].join('\n');
344
+ summaryMeta = { label: 'monograph-graph-summary', source: 'monograph.db', nodes: nodeC, edges: edgeC };
345
+ } finally {
346
+ try { sumDb.close(); } catch (_) {}
347
+ }
348
+ }
349
+ } catch (e) { /* fall through to legacy */ }
350
+ }
351
+
352
+ if (!summaryText && fs.existsSync(legacyStats2) && fs.existsSync(legacyGraph2)) {
353
+ try {
354
+ var lStats = JSON.parse(fs.readFileSync(legacyStats2, 'utf-8'));
355
+ var lGraphStat = fs.statSync(legacyGraph2);
356
+ if (lGraphStat.size < 10 * 1024 * 1024) {
357
+ var lGraph = JSON.parse(fs.readFileSync(legacyGraph2, 'utf-8'));
358
+ var lNodes = Array.isArray(lGraph.nodes) ? lGraph.nodes : [];
359
+ summaryText = 'MONOGRAPH KNOWLEDGE GRAPH SUMMARY (legacy JSON)\n' +
360
+ 'Nodes: ' + (lStats.nodes || lNodes.length) + ' | Edges: ' + (lStats.edges || 0) + '\n' +
361
+ 'Use mcp__monomind__monograph_suggest to find files relevant to your task.';
362
+ summaryMeta = { label: 'monograph-graph-summary', source: 'legacy-json', builtAt: lStats.builtAt };
363
+ }
364
+ } catch (e) { /* ignore */ }
365
+ }
366
+
367
+ if (summaryText) {
368
+ var chunkId = crypto.createHash('md5').update('monograph-graph-summary').digest('hex').slice(0, 16);
369
+ newLines.push(JSON.stringify({
370
+ chunkId: chunkId,
371
+ namespace: 'knowledge:shared',
372
+ text: summaryText,
373
+ metadata: summaryMeta
374
+ }));
350
375
  }
351
376
  } catch (e) { /* graph not available yet, skip */ }
352
377
 
@@ -639,14 +664,34 @@ const handlers = {
639
664
 
640
665
  console.log(output.join('\n'));
641
666
 
642
- // Inject monograph hint for complex tasks
667
+ // Inject monograph hint for complex tasks.
668
+ // Source of truth is .monomind/monograph.db (SQLite). Legacy stats.json
669
+ // is no longer written by the build, so it is checked only as a fallback.
643
670
  try {
644
- var graphStatsPath = path.join(CWD, '.monomind', 'graph', 'stats.json');
645
- if (fs.existsSync(graphStatsPath)) {
646
- var gStats = JSON.parse(fs.readFileSync(graphStatsPath, 'utf-8'));
647
- if (gStats.nodes > 100) {
648
- console.log('\n[MONOGRAPH] ' + gStats.nodes + ' nodes indexed. For this task, call mcp__monomind__monograph_suggest first.');
649
- }
671
+ var monographDb = path.join(CWD, '.monomind', 'monograph.db');
672
+ var legacyStats = path.join(CWD, '.monomind', 'graph', 'stats.json');
673
+ var nodeCount = 0;
674
+ if (fs.existsSync(monographDb)) {
675
+ try {
676
+ var mgMod = _requireMonograph();
677
+ if (mgMod && mgMod.openDb) {
678
+ var hintDb = mgMod.openDb(monographDb);
679
+ try {
680
+ nodeCount = hintDb.prepare('SELECT COUNT(*) AS c FROM nodes').get().c;
681
+ } finally {
682
+ try { hintDb.close(); } catch (_) {}
683
+ }
684
+ }
685
+ } catch (e) { /* ignore — fall back to legacy */ }
686
+ }
687
+ if (nodeCount === 0 && fs.existsSync(legacyStats)) {
688
+ try {
689
+ var gStats = JSON.parse(fs.readFileSync(legacyStats, 'utf-8'));
690
+ nodeCount = gStats.nodes || 0;
691
+ } catch (e) { /* ignore */ }
692
+ }
693
+ if (nodeCount > 100) {
694
+ console.log('\n[MONOGRAPH] ' + nodeCount + ' nodes indexed. For this task, call mcp__monomind__monograph_suggest first to find the right files without grepping.');
650
695
  }
651
696
  } catch(e) {}
652
697
 
@@ -696,6 +696,78 @@ function getGraphifyStats() {
696
696
  return { nodes: 0, edges: 0, exists: false };
697
697
  }
698
698
 
699
+ // Graph freshness — compare graph build time against most recent commit
700
+ // Returns: { commitsBehind, stale } where stale = >5 commits or never built
701
+ function getGraphFreshness() {
702
+ const lockPath = path.join(CWD, '.monomind', 'graph', '.rebuild-lock');
703
+ const dbPath = path.join(CWD, '.monomind', 'monograph.db');
704
+
705
+ let buildMs = 0;
706
+ try {
707
+ const lockStat = safeStat(lockPath);
708
+ const dbStat = safeStat(dbPath);
709
+ buildMs = Math.max(lockStat?.mtimeMs || 0, dbStat?.mtimeMs || 0);
710
+ } catch { /* ignore */ }
711
+ if (!buildMs) return { commitsBehind: -1, stale: true, fresh: false };
712
+
713
+ // Count commits since the graph was last built
714
+ const buildIso = new Date(buildMs).toISOString();
715
+ const out = safeExec(`git rev-list --count --since='${buildIso}' HEAD 2>/dev/null`, 1500);
716
+ const commitsBehind = parseInt(out, 10) || 0;
717
+ return {
718
+ commitsBehind,
719
+ stale: commitsBehind > 5,
720
+ fresh: commitsBehind === 0,
721
+ };
722
+ }
723
+
724
+ // Active loops — scan .monomind/loops/*.json
725
+ // Filters: skip *-hil*.json, skip stale (>6h since lastRunAt)
726
+ function getLoopStatus() {
727
+ const loopsDir = path.join(CWD, '.monomind', 'loops');
728
+ if (!fs.existsSync(loopsDir)) return { count: 0, loops: [] };
729
+ const STALE_MS = 6 * 60 * 60 * 1000;
730
+ const now = Date.now();
731
+ let loops = [];
732
+ try {
733
+ const files = fs.readdirSync(loopsDir).filter(f =>
734
+ f.endsWith('.json') && !f.includes('-hil') && !f.endsWith('.stop'));
735
+ for (const f of files) {
736
+ const d = readJSON(path.join(loopsDir, f));
737
+ if (!d || !d.command) continue;
738
+ const last = d.lastRunAt || d.nextRunAt || d.startedAt || 0;
739
+ if (last && (now - last) > STALE_MS) continue;
740
+ loops.push({
741
+ cmd: d.command.replace(/^\//,''),
742
+ type: d.type || 'repeat',
743
+ rep: d.currentRep || 0,
744
+ max: d.maxReps || 0,
745
+ status: d.status || 'running',
746
+ });
747
+ }
748
+ } catch { /* ignore */ }
749
+ return { count: loops.length, loops };
750
+ }
751
+
752
+ // HIL pending — count <id>-hil.md files with no human response yet
753
+ function getHILPending() {
754
+ const loopsDir = path.join(CWD, '.monomind', 'loops');
755
+ if (!fs.existsSync(loopsDir)) return { pending: 0 };
756
+ let pending = 0;
757
+ try {
758
+ const files = fs.readdirSync(loopsDir).filter(f => f.endsWith('-hil.md'));
759
+ for (const f of files) {
760
+ try {
761
+ const txt = fs.readFileSync(path.join(loopsDir, f), 'utf-8');
762
+ // A response is a line starting with "> " followed by non-whitespace
763
+ const answered = /^[ \t]*>[ \t]+\S/m.test(txt);
764
+ if (!answered) pending++;
765
+ } catch { /* ignore */ }
766
+ }
767
+ } catch { /* ignore */ }
768
+ return { pending };
769
+ }
770
+
699
771
  // Memory Palace stats — drawers.jsonl + kg.json (the real persistent memory)
700
772
  function getMemoryPalaceStats() {
701
773
  const palaceDir = path.join(CWD, '.monomind', 'palace');
@@ -1130,158 +1202,68 @@ function generateDashboard() {
1130
1202
  lines.push(hdr);
1131
1203
  lines.push(SEP);
1132
1204
 
1133
- // ── Row 1: Knowledge & Graphify ──────────────────────────────
1134
- const knowStr = knowledge.chunks > 0
1135
- ? `${x.teal}📚 ${x.bold}${knowledge.chunks}${x.reset}${x.slate} chunks${x.reset}`
1136
- : `${x.slate}📚 no chunks${x.reset}`;
1137
-
1138
- const skillStr = knowledge.skills > 0
1139
- ? ` ${x.mint}✦ ${knowledge.skills} skills${x.reset}`
1140
- : '';
1141
-
1142
- const patStr = progress.patternsLearned > 0
1143
- ? `${x.gold}${progress.patternsLearned >= 1000 ? (progress.patternsLearned / 1000).toFixed(1) + 'k' : progress.patternsLearned} patterns${x.reset}`
1144
- : `${x.slate}0 patterns${x.reset}`;
1145
-
1146
- const gf = getGraphifyStats();
1147
- const gfStr = gf.exists
1148
- ? `${x.sky}🔗 ${x.bold}${gf.nodes}${x.reset}${x.slate} nodes · ${x.reset}${x.sky}${x.bold}${gf.edges}${x.reset}${x.slate} edges${x.reset}`
1149
- : `${x.slate}🔗 no graph${x.reset}`;
1150
-
1151
- lines.push(
1152
- `${x.purple}💡 INTEL${x.reset} ` +
1153
- `${knowStr}${skillStr} ${DIV} ` +
1154
- `${patStr} ${DIV} ` +
1155
- gfStr
1156
- );
1157
- lines.push(SEP);
1158
-
1159
- // ── Row 2: Agents & Triggers ──────────────────────────────────
1160
- const agentCol = swarm.activeAgents > 0 ? x.green : x.slate;
1161
- const hookCol = hooks.enabled > 0 ? x.mint : x.slate;
1162
-
1163
- // Triggers (Task 32)
1164
- const trigStr = triggers.triggers > 0
1165
- ? `${x.mint}🎯 ${x.bold}${triggers.triggers}${x.reset}${x.slate} triggers · ${triggers.agents} agents${x.reset}`
1166
- : `${x.slate}🎯 no triggers${x.reset}`;
1167
-
1168
- // Active agent badge
1169
- let agentBadge;
1205
+ // ── Row 1: Active agent + Loop status ────────────────────────
1206
+ let agentStr;
1170
1207
  if (activeAgent) {
1171
1208
  const isExtras = activeAgent.slug === 'extras' || activeAgent.name === 'Extras';
1172
1209
  if (isExtras && activeAgent.extrasMatches && activeAgent.extrasMatches.length > 0) {
1173
- // Show specific specialist names instead of generic "Extras"
1174
- const specialists = activeAgent.extrasMatches.slice(0, 3);
1175
- const badgeParts = specialists.map(s => `${x.sky}👤 ${x.bold}${s.name}${x.reset}`);
1176
- agentBadge = badgeParts.join(`${x.slate} ${x.reset}`);
1210
+ const specialists = activeAgent.extrasMatches.slice(0, 3).map(s => s.name).join(', ');
1211
+ agentStr = `${x.sky}👤 ${x.bold}${specialists}${x.reset}`;
1177
1212
  } else if (isExtras) {
1178
- // "extras" with no specific matches — suppress
1179
- agentBadge = `${x.slate}👤 no agent routed${x.reset}`;
1213
+ agentStr = `${x.slate}👤 no agent routed${x.reset}`;
1180
1214
  } else {
1181
1215
  const col = activeAgent.activated ? x.green : x.sky;
1182
- const mark = activeAgent.activated ? '● ACTIVE' : '';
1216
+ const mark = activeAgent.activated ? `${col}${x.bold}● ACTIVE${x.reset} ` : '';
1183
1217
  const conf = activeAgent.activated ? '' : ` ${x.slate}${(activeAgent.confidence * 100).toFixed(0)}%${x.reset}`;
1184
- const cat = activeAgent.category ? ` ${x.slate}[${activeAgent.category}]${x.reset}` : '';
1185
- agentBadge = mark
1186
- ? `${col}${x.bold}${mark}${x.reset} ${col}👤 ${x.bold}${activeAgent.name}${x.reset}${cat}${conf}`
1187
- : `${col}👤 ${x.bold}${activeAgent.name}${x.reset}${cat}${conf}`;
1218
+ agentStr = `${mark}${col}👤 ${x.bold}${activeAgent.name}${x.reset}${conf}`;
1188
1219
  }
1189
1220
  } else {
1190
- agentBadge = `${x.slate}👤 no agent routed${x.reset}`;
1221
+ agentStr = `${x.slate}👤 no agent routed${x.reset}`;
1191
1222
  }
1192
1223
 
1193
- // Swarm line: show active count, or "N ✓ idle" when recently used, or "idle" when never used
1194
- const swarmCountStr = swarm.activeAgents > 0
1195
- ? `${agentCol}${x.bold}${swarm.activeAgents}${x.reset}${x.slate}/${x.reset}${x.white}${swarm.maxAgents}${x.reset} agents`
1196
- : (swarm.lastActive || 0) > 0
1197
- ? `${x.slate}${swarm.lastActive}${x.reset}${x.slate}/${swarm.maxAgents} (${x.reset}${x.green}✓ done${x.slate})${x.reset}`
1198
- : `${x.slate}idle${x.reset} `;
1199
- lines.push(
1200
- `${x.gold}🐝 SWARM${x.reset} ` +
1201
- `${swarmCountStr} ` +
1202
- `${hookCol} ${hooks.enabled}/${hooks.total} hooks${x.reset} ${DIV} ` +
1203
- `${trigStr} ${DIV} ` +
1204
- agentBadge
1205
- );
1206
- lines.push(SEP);
1207
-
1208
- // ── Row 3: Security ──────────────────────────────────────────
1209
- const cveStatus = security.totalCves === 0
1210
- ? (security.status === 'NONE' ? `${x.slate}not scanned${x.reset}` : `${x.green}✔ clean${x.reset}`)
1211
- : `${x.coral}${security.cvesFixed}/${security.totalCves} fixed${x.reset}`;
1212
-
1213
- lines.push(
1214
- `${x.purple}🛡️ SECURITY${x.reset} ` +
1215
- `${sec.col}${sec.label}${x.reset} ${DIV} ` +
1216
- `CVE ${cveStatus}`
1217
- );
1218
- lines.push(SEP);
1219
-
1220
- // ── Row 4: Memory & Tests ─────────────────────────────────────
1221
- const testCol = tests.testFiles > 0 ? x.green : x.slate;
1222
- const memCol = system.memoryMB > 200 ? x.orange : x.sky;
1223
-
1224
- // Auto-memory files display
1225
- const memFileCol = autoMem.count > 0 ? x.purple : x.slate;
1226
- const memFileStr = autoMem.count > 0
1227
- ? `${memFileCol}${x.bold}${autoMem.count}${x.reset}${x.slate} memories${x.reset}`
1228
- : `${x.slate}no memories${x.reset}`;
1229
- const memTypeOrder = ['handoff', 'user', 'feedback', 'project', 'reference'];
1230
- const typeColors = { user: x.sky, feedback: x.gold, project: x.teal, reference: x.violet, handoff: x.coral };
1231
- const typeParts = memTypeOrder
1232
- .filter(t => autoMem.byType[t] > 0)
1233
- .map(t => `${typeColors[t] || x.slate}${autoMem.byType[t]}${t.slice(0,1)}${x.reset}`);
1234
- const kgStr = typeParts.length > 0 ? ` ${typeParts.join(' ')}` : '';
1235
-
1236
- // Total memory size (palace + agentdb)
1237
- const totalSizeKB = agentdb.dbSizeKB + palace.palaceSizeKB;
1238
- const sizeDisp = totalSizeKB >= 1024
1239
- ? `${(totalSizeKB / 1024).toFixed(1)} MB` : `${totalSizeKB} KB`;
1240
-
1241
- // HNSW tag only meaningful if vectors exist
1242
- const vecTotal = agentdb.vectorCount;
1243
- const hnswTag = agentdb.hasHnsw ? ` ${x.green}⚡ HNSW${x.reset}` : '';
1244
-
1245
- const chips = [];
1246
- if (integration.mcpServers.total > 0) {
1247
- const mc = integration.mcpServers.enabled === integration.mcpServers.total ? x.green
1248
- : integration.mcpServers.enabled > 0 ? x.gold : x.coral;
1249
- chips.push(`${mc}MCP ${integration.mcpServers.enabled}/${integration.mcpServers.total}${x.reset}`);
1224
+ const loopState = getLoopStatus();
1225
+ let loopStr;
1226
+ if (loopState.count > 0) {
1227
+ const parts = loopState.loops.slice(0, 2).map(l => {
1228
+ const status = l.status === 'hil:pending'
1229
+ ? `${x.coral}⏳ HIL${x.reset}`
1230
+ : `${x.green}⟳${x.reset}`;
1231
+ const tag = l.type === 'tillend'
1232
+ ? `${x.bold}${l.cmd}${x.reset}${x.slate} run ${l.rep}${x.reset}`
1233
+ : `${x.bold}${l.cmd}${x.reset}${x.slate} ${l.rep}/${l.max}${x.reset}`;
1234
+ return `${status} ${tag}`;
1235
+ });
1236
+ loopStr = `${x.gold}🔄${x.reset} ${parts.join(`${x.slate} · ${x.reset}`)}`;
1237
+ if (loopState.count > 2) loopStr += `${x.slate} +${loopState.count - 2} more${x.reset}`;
1238
+ } else {
1239
+ loopStr = `${x.slate}🔄 no active loops${x.reset}`;
1250
1240
  }
1251
- if (integration.hasDatabase) chips.push(`${x.green}DB ✔${x.reset}`);
1252
- if (integration.hasApi) chips.push(`${x.green}API ✔${x.reset}`);
1253
- const integStr = chips.length ? chips.join(' ') : `${x.slate}none${x.reset}`;
1254
-
1255
- lines.push(
1256
- `${x.teal}🗄️ MEMORY${x.reset} ` +
1257
- `${memFileStr}${kgStr}${hnswTag} ${DIV} ` +
1258
- `${x.white}${sizeDisp}${x.reset} ${DIV} ` +
1259
- `${testCol}🧪 ${tests.testFiles} test files${x.reset} ${DIV} ` +
1260
- integStr
1261
- );
1241
+
1242
+ lines.push(`${x.purple}🤖 AGENT${x.reset} ${agentStr} ${DIV} ${loopStr}`);
1262
1243
  lines.push(SEP);
1263
1244
 
1264
- // ── Row 5: Context budget ─────────────────────────────────────
1265
- // SI budget (Task 23 monitor)
1266
- let siStr;
1267
- if (si) {
1268
- const siCol = si.pct > 100 ? x.coral : si.pct > 80 ? x.gold : x.green;
1269
- siStr = `${siCol}📄 SI ${si.pct}% budget${x.reset} ${x.dim}(${si.len}/${si.limit} chars)${x.reset}`;
1245
+ // ── Row 2: Graph freshness + Pending HIL ─────────────────────
1246
+ const gf = getGraphifyStats();
1247
+ const freshness = getGraphFreshness();
1248
+ let graphStr;
1249
+ if (gf.exists) {
1250
+ const nodesFmt = gf.nodes >= 1000 ? `${(gf.nodes / 1000).toFixed(0)}k` : `${gf.nodes}`;
1251
+ const freshTag = freshness.fresh
1252
+ ? `${x.green}● fresh${x.reset}`
1253
+ : freshness.stale
1254
+ ? `${x.coral}● ${freshness.commitsBehind} commits stale${x.reset}`
1255
+ : `${x.gold}● ${freshness.commitsBehind} behind${x.reset}`;
1256
+ graphStr = `${x.sky}🔗 ${x.bold}${nodesFmt}${x.reset}${x.slate} nodes${x.reset} ${freshTag}`;
1270
1257
  } else {
1271
- siStr = `${x.slate}📄 no shared instructions${x.reset}`;
1258
+ graphStr = `${x.slate}🔗 no graph${x.reset}`;
1272
1259
  }
1273
1260
 
1274
- let monthStr = '';
1275
- if (tokens) {
1276
- const mFmt = tokens.monthCost >= 100 ? `$${tokens.monthCost.toFixed(2)}` : tokens.monthCost >= 1 ? `$${tokens.monthCost.toFixed(3)}` : `$${tokens.monthCost.toFixed(4)}`;
1277
- monthStr = ` ${DIV} ${x.gold}📈 ${x.bold}${mFmt}${x.reset}${x.slate} month · ${tokens.monthCalls} calls${x.reset}`;
1278
- }
1279
- lines.push(
1280
- `${x.slate}📋 CONTEXT${x.reset} ` +
1281
- `${siStr} ${DIV} ` +
1282
- `${x.dim}💾 ${system.memoryMB} MB RAM${x.reset}` +
1283
- monthStr
1284
- );
1261
+ const hil = getHILPending();
1262
+ const hilStr = hil.pending > 0
1263
+ ? `${x.coral} ${x.bold}${hil.pending}${x.reset}${x.coral} HIL pending${x.reset}`
1264
+ : `${x.slate} no pending HIL${x.reset}`;
1265
+
1266
+ lines.push(`${x.teal}🧠 CONTEXT${x.reset} ${graphStr} ${DIV} ${hilStr}`);
1285
1267
 
1286
1268
  return lines.join('\n');
1287
1269
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "monomind",
3
- "version": "1.10.6",
3
+ "version": "1.10.8",
4
4
  "description": "Monomind - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -828,6 +828,68 @@ function getTriggerStats() {
828
828
  } catch { return { triggers: 0, agents: 0 }; }
829
829
  }
830
830
 
831
+ // Graph freshness — compare last build time vs commits since
832
+ function getGraphFreshness() {
833
+ const lockPath = path.join(CWD, '.monomind', 'graph', '.rebuild-lock');
834
+ const dbPath = path.join(CWD, '.monomind', 'monograph.db');
835
+ let buildMs = 0;
836
+ try {
837
+ const lockStat = safeStat(lockPath);
838
+ const dbStat = safeStat(dbPath);
839
+ buildMs = Math.max(lockStat?.mtimeMs || 0, dbStat?.mtimeMs || 0);
840
+ } catch { /* ignore */ }
841
+ if (!buildMs) return { commitsBehind: -1, stale: true, fresh: false };
842
+ const buildIso = new Date(buildMs).toISOString();
843
+ const out = safeExec(\`git rev-list --count --since='\${buildIso}' HEAD 2>/dev/null\`, 1500);
844
+ const commitsBehind = parseInt(out, 10) || 0;
845
+ return { commitsBehind, stale: commitsBehind > 5, fresh: commitsBehind === 0 };
846
+ }
847
+
848
+ // Active loops — scan .monomind/loops/*.json, skip stale (>6h)
849
+ function getLoopStatus() {
850
+ const loopsDir = path.join(CWD, '.monomind', 'loops');
851
+ if (!fs.existsSync(loopsDir)) return { count: 0, loops: [] };
852
+ const STALE_MS = 6 * 60 * 60 * 1000;
853
+ const now = Date.now();
854
+ const loops = [];
855
+ try {
856
+ const files = fs.readdirSync(loopsDir).filter(f =>
857
+ f.endsWith('.json') && !f.includes('-hil') && !f.endsWith('.stop'));
858
+ for (const f of files) {
859
+ const d = readJSON(path.join(loopsDir, f));
860
+ if (!d || !d.command) continue;
861
+ const last = d.lastRunAt || d.nextRunAt || d.startedAt || 0;
862
+ if (last && (now - last) > STALE_MS) continue;
863
+ loops.push({
864
+ cmd: String(d.command).replace(/^\\//,''),
865
+ type: d.type || 'repeat',
866
+ rep: d.currentRep || 0,
867
+ max: d.maxReps || 0,
868
+ status: d.status || 'running',
869
+ });
870
+ }
871
+ } catch { /* ignore */ }
872
+ return { count: loops.length, loops };
873
+ }
874
+
875
+ // HIL pending — count <id>-hil.md files with no human response yet
876
+ function getHILPending() {
877
+ const loopsDir = path.join(CWD, '.monomind', 'loops');
878
+ if (!fs.existsSync(loopsDir)) return { pending: 0 };
879
+ let pending = 0;
880
+ try {
881
+ const files = fs.readdirSync(loopsDir).filter(f => f.endsWith('-hil.md'));
882
+ for (const f of files) {
883
+ try {
884
+ const txt = fs.readFileSync(path.join(loopsDir, f), 'utf-8');
885
+ const answered = /^[ \\t]*>[ \\t]+\\S/m.test(txt);
886
+ if (!answered) pending++;
887
+ } catch { /* ignore */ }
888
+ }
889
+ } catch { /* ignore */ }
890
+ return { pending };
891
+ }
892
+
831
893
  // Monograph knowledge graph stats
832
894
  // Sources, in priority order:
833
895
  // 1. .monomind/graph/stats.json — explicit cached stats
@@ -979,144 +1041,60 @@ function generateDashboard() {
979
1041
  lines.push(hdr);
980
1042
  lines.push(SEP);
981
1043
 
982
- // ── Row 1: Intelligence & Learning ───────────────────────────
983
- const intellCol = pctColor(system.intelligencePct);
984
- const intellBar = blockBar(system.intelligencePct, 100, 6);
985
-
986
- // Knowledge (Task 28)
987
- const knowStr = knowledge.chunks > 0
988
- ? \`\${x.teal}📚 \${x.bold}\${knowledge.chunks}\${x.reset}\${x.slate} chunks\${x.reset}\`
989
- : \`\${x.slate}📚 no chunks\${x.reset}\`;
990
-
991
- // Skills (Task 45)
992
- const skillStr = knowledge.skills > 0
993
- ? \` \${x.mint}✦ \${knowledge.skills} skills\${x.reset}\`
994
- : '';
995
-
996
- // Patterns
997
- const patStr = progress.patternsLearned > 0
998
- ? \`\${x.gold}\${progress.patternsLearned >= 1000 ? (progress.patternsLearned / 1000).toFixed(1) + 'k' : progress.patternsLearned} patterns\${x.reset}\`
999
- : \`\${x.slate}0 patterns\${x.reset}\`;
1000
-
1001
- // Graph (monograph)
1002
- const graph = getGraphifyStats();
1003
- const graphStr = graph.exists
1004
- ? \`\${x.sky}🔗 \${x.bold}\${graph.nodes}\${x.reset}\${x.slate} nodes · \${x.reset}\${x.sky}\${x.bold}\${graph.edges}\${x.reset}\${x.slate} edges\${x.reset}\`
1005
- : \`\${x.slate}🔗 no graph\${x.reset}\`;
1006
-
1007
- lines.push(
1008
- \`\${x.purple}💡 INTEL\${x.reset} \` +
1009
- \`\${intellCol}\${intellBar} \${x.bold}\${system.intelligencePct}%\${x.reset} \${DIV} \` +
1010
- \`\${knowStr}\${skillStr} \${DIV} \` +
1011
- \`\${patStr} \${DIV} \` +
1012
- graphStr
1013
- );
1014
- lines.push(SEP);
1015
-
1016
- // ── Row 2: Agents & Triggers ──────────────────────────────────
1017
- const agentCol = swarm.activeAgents > 0 ? x.green : x.slate;
1018
- const hookCol = hooks.enabled > 0 ? x.mint : x.slate;
1019
-
1020
- // Triggers (Task 32)
1021
- const trigStr = triggers.triggers > 0
1022
- ? \`\${x.mint}🎯 \${x.bold}\${triggers.triggers}\${x.reset}\${x.slate} triggers · \${triggers.agents} agents\${x.reset}\`
1023
- : \`\${x.slate}🎯 no triggers\${x.reset}\`;
1024
-
1025
- // Active agent badge
1026
- let agentBadge;
1044
+ // ── Row 1: Active agent + Loop status ────────────────────────
1045
+ let agentStr;
1027
1046
  if (activeAgent) {
1028
1047
  const col = activeAgent.activated ? x.green : x.sky;
1029
- const mark = activeAgent.activated ? '● ACTIVE' : '→ ROUTED';
1048
+ const mark = activeAgent.activated ? \`\${col}\${x.bold}● ACTIVE\${x.reset} \` : '';
1030
1049
  const conf = activeAgent.activated ? '' : \` \${x.slate}\${(activeAgent.confidence * 100).toFixed(0)}%\${x.reset}\`;
1031
- const cat = activeAgent.category ? \` \${x.slate}[\${activeAgent.category}]\${x.reset}\` : '';
1032
- agentBadge = \`\${col}\${x.bold}\${mark}\${x.reset} \${col}👤 \${x.bold}\${activeAgent.name}\${x.reset}\${cat}\${conf}\`;
1050
+ agentStr = \`\${mark}\${col}👤 \${x.bold}\${activeAgent.name}\${x.reset}\${conf}\`;
1033
1051
  } else {
1034
- agentBadge = \`\${x.slate}👤 no agent routed\${x.reset}\`;
1052
+ agentStr = \`\${x.slate}👤 no agent routed\${x.reset}\`;
1035
1053
  }
1036
1054
 
1037
- lines.push(
1038
- \`\${x.gold}🐝 SWARM\${x.reset} \` +
1039
- \`\${agentCol}\${x.bold}\${swarm.activeAgents}\${x.reset}\${x.slate}/\${x.reset}\${x.white}\${swarm.maxAgents}\${x.reset} agents \` +
1040
- \`\${hookCol}⚡ \${hooks.enabled}/\${hooks.total} hooks\${x.reset} \${DIV} \` +
1041
- \`\${trigStr} \${DIV} \` +
1042
- agentBadge
1043
- );
1044
- lines.push(SEP);
1045
-
1046
- // ── Row 3: Architecture & Security ───────────────────────────
1047
- const adrCol = adrs.count > 0
1048
- ? (adrs.implemented >= adrs.count ? x.green : x.gold)
1049
- : x.slate;
1050
- const adrStr = adrs.count > 0
1051
- ? \`\${adrCol}\${x.bold}\${adrs.implemented}\${x.reset}\${x.slate}/\${x.reset}\${x.white}\${adrs.count}\${x.reset} ADRs\`
1052
- : \`\${x.slate}no ADRs\${x.reset}\`;
1053
-
1054
- const dddCol = pctColor(progress.dddProgress);
1055
- const dddBar = blockBar(progress.dddProgress, 100, 5);
1056
-
1057
- const cveStatus = security.totalCves === 0
1058
- ? (security.status === 'NONE' ? \`\${x.slate}not scanned\${x.reset}\` : \`\${x.green}✔ clean\${x.reset}\`)
1059
- : \`\${x.coral}\${security.cvesFixed}/\${security.totalCves} fixed\${x.reset}\`;
1060
-
1061
- lines.push(
1062
- \`\${x.purple}🧩 ARCH\${x.reset} \` +
1063
- \`\${adrStr} \${DIV} \` +
1064
- \`DDD \${dddBar} \${dddCol}\${x.bold}\${progress.dddProgress}%\${x.reset} \${DIV} \` +
1065
- \`🛡️ \${sec.col}\${sec.label}\${x.reset} \${DIV} \` +
1066
- \`CVE \${cveStatus}\`
1067
- );
1068
- lines.push(SEP);
1069
-
1070
- // ── Row 4: Memory & Tests ─────────────────────────────────────
1071
- const vecCol = agentdb.vectorCount > 0 ? x.green : x.slate;
1072
- const hnswTag = agentdb.hasHnsw && agentdb.vectorCount > 0 ? \` \${x.green}⚡ HNSW\${x.reset}\` : '';
1073
- const sizeDisp = agentdb.dbSizeKB >= 1024
1074
- ? \`\${(agentdb.dbSizeKB / 1024).toFixed(1)} MB\` : \`\${agentdb.dbSizeKB} KB\`;
1075
- const testCol = tests.testFiles > 0 ? x.green : x.slate;
1076
- const memCol = system.memoryMB > 200 ? x.orange : x.sky;
1077
-
1078
- const chips = [];
1079
- if (integration.mcpServers.total > 0) {
1080
- const mc = integration.mcpServers.enabled === integration.mcpServers.total ? x.green
1081
- : integration.mcpServers.enabled > 0 ? x.gold : x.coral;
1082
- chips.push(\`\${mc}MCP \${integration.mcpServers.enabled}/\${integration.mcpServers.total}\${x.reset}\`);
1055
+ const loopState = getLoopStatus();
1056
+ let loopStr;
1057
+ if (loopState.count > 0) {
1058
+ const parts = loopState.loops.slice(0, 2).map(l => {
1059
+ const status = l.status === 'hil:pending'
1060
+ ? \`\${x.coral}⏳ HIL\${x.reset}\`
1061
+ : \`\${x.green}⟳\${x.reset}\`;
1062
+ const tag = l.type === 'tillend'
1063
+ ? \`\${x.bold}\${l.cmd}\${x.reset}\${x.slate} run \${l.rep}\${x.reset}\`
1064
+ : \`\${x.bold}\${l.cmd}\${x.reset}\${x.slate} \${l.rep}/\${l.max}\${x.reset}\`;
1065
+ return \`\${status} \${tag}\`;
1066
+ });
1067
+ loopStr = \`\${x.gold}🔄\${x.reset} \${parts.join(\`\${x.slate} · \${x.reset}\`)}\`;
1068
+ if (loopState.count > 2) loopStr += \`\${x.slate} +\${loopState.count - 2} more\${x.reset}\`;
1069
+ } else {
1070
+ loopStr = \`\${x.slate}🔄 no active loops\${x.reset}\`;
1083
1071
  }
1084
- if (integration.hasDatabase) chips.push(\`\${x.green}DB ✔\${x.reset}\`);
1085
- if (integration.hasApi) chips.push(\`\${x.green}API ✔\${x.reset}\`);
1086
- const integStr = chips.length ? chips.join(' ') : \`\${x.slate}none\${x.reset}\`;
1087
-
1088
- lines.push(
1089
- \`\${x.teal}🗄️ MEMORY\${x.reset} \` +
1090
- \`\${vecCol}\${x.bold}\${agentdb.vectorCount}\${x.reset}\${x.slate} vectors\${x.reset}\${hnswTag} \${DIV} \` +
1091
- \`\${x.white}\${sizeDisp}\${x.reset} \${DIV} \` +
1092
- \`\${testCol}🧪 \${tests.testFiles} test files\${x.reset} \${DIV} \` +
1093
- integStr
1094
- );
1072
+
1073
+ lines.push(\`\${x.purple}🤖 AGENT\${x.reset} \${agentStr} \${DIV} \${loopStr}\`);
1095
1074
  lines.push(SEP);
1096
1075
 
1097
- // ── Row 5: Context budget ─────────────────────────────────────
1098
- // SI budget (Task 23 monitor)
1099
- let siStr;
1100
- if (si) {
1101
- const siCol = si.pct > 100 ? x.coral : si.pct > 80 ? x.gold : x.green;
1102
- siStr = \`\${siCol}📄 SI \${si.pct}% budget\${x.reset} \${x.dim}(\${si.len}/\${si.limit} chars)\${x.reset}\`;
1076
+ // ── Row 2: Graph freshness + Pending HIL ─────────────────────
1077
+ const gf = getGraphifyStats();
1078
+ const freshness = getGraphFreshness();
1079
+ let graphStr;
1080
+ if (gf.exists) {
1081
+ const nodesFmt = gf.nodes >= 1000 ? \`\${(gf.nodes / 1000).toFixed(0)}k\` : \`\${gf.nodes}\`;
1082
+ const freshTag = freshness.fresh
1083
+ ? \`\${x.green}● fresh\${x.reset}\`
1084
+ : freshness.stale
1085
+ ? \`\${x.coral}● \${freshness.commitsBehind} commits stale\${x.reset}\`
1086
+ : \`\${x.gold}● \${freshness.commitsBehind} behind\${x.reset}\`;
1087
+ graphStr = \`\${x.sky}🔗 \${x.bold}\${nodesFmt}\${x.reset}\${x.slate} nodes\${x.reset} \${freshTag}\`;
1103
1088
  } else {
1104
- siStr = \`\${x.slate}📄 no shared instructions\${x.reset}\`;
1089
+ graphStr = \`\${x.slate}🔗 no graph\${x.reset}\`;
1105
1090
  }
1106
1091
 
1107
- // Domains
1108
- const domCol = progress.domainsCompleted >= 4 ? x.green
1109
- : progress.domainsCompleted >= 2 ? x.gold
1110
- : progress.domainsCompleted >= 1 ? x.orange
1111
- : x.slate;
1112
- const domBar = blockBar(progress.domainsCompleted, progress.totalDomains);
1113
-
1114
- lines.push(
1115
- \`\${x.slate}📋 CONTEXT\${x.reset} \` +
1116
- \`\${siStr} \${DIV} \` +
1117
- \`\${x.teal}🏗 \${domBar} \${domCol}\${x.bold}\${progress.domainsCompleted}\${x.reset}\${x.slate}/\${x.reset}\${x.white}\${progress.totalDomains}\${x.reset} domains \${DIV} \` +
1118
- \`\${x.dim}💾 \${system.memoryMB} MB RAM\${x.reset}\`
1119
- );
1092
+ const hil = getHILPending();
1093
+ const hilStr = hil.pending > 0
1094
+ ? \`\${x.coral}✨ \${x.bold}\${hil.pending}\${x.reset}\${x.coral} HIL pending\${x.reset}\`
1095
+ : \`\${x.slate}✨ no pending HIL\${x.reset}\`;
1096
+
1097
+ lines.push(\`\${x.teal}🧠 CONTEXT\${x.reset} \${graphStr} \${DIV} \${hilStr}\`);
1120
1098
 
1121
1099
  return lines.join('\\n');
1122
1100
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoes/monomindcli",
3
- "version": "1.10.6",
3
+ "version": "1.10.8",
4
4
  "type": "module",
5
5
  "description": "Monomind CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",