@monoes/monomindcli 1.10.10 → 1.10.12

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.
@@ -0,0 +1,11 @@
1
+ ---
2
+ description: Draft an Architecture Decision Record from accumulated decision markers in this session's prompts
3
+ ---
4
+
5
+ Run the hook handler's adr-draft action to scan `.monomind/decisions.jsonl` for the last 7 days of decision markers (e.g. "let's go with X", "we chose Y", "decision: Z") and produce an ADR template in `docs/adrs/`.
6
+
7
+ ```bash
8
+ node "$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs" adr-draft
9
+ ```
10
+
11
+ After running, open the generated `docs/adrs/ADR-NNNN-YYYY-MM-DD-session-decisions.md`, fill in the Context and Consequences sections, and change Status to Accepted/Rejected.
@@ -62,6 +62,8 @@ function getMonographSuggestions(taskText, limit) {
62
62
  try {
63
63
  // BM25 ranks better than degree for keyword relevance; tie-break by deg.
64
64
  // File/Function/Class outrank Section so navigable nodes win.
65
+ // Filter out anonymous lambdas, arrow expressions, and other unnamed
66
+ // garbage that the AST extraction picks up but isn't navigable.
65
67
  rows = db.prepare(
66
68
  "SELECT n.id, n.name, n.label, n.file_path AS file, " +
67
69
  "bm25(nodes_fts) AS bm25_score, " +
@@ -71,6 +73,8 @@ function getMonographSuggestions(taskText, limit) {
71
73
  "FROM nodes_fts f JOIN nodes n ON f.rowid = n.rowid " +
72
74
  "WHERE nodes_fts MATCH ? AND n.file_path IS NOT NULL AND n.file_path != '' " +
73
75
  "AND n.label NOT IN ('Concept') " +
76
+ "AND n.name NOT LIKE '(%' AND n.name NOT LIKE '%=>%' AND n.name != 'function' " +
77
+ "AND length(n.name) >= 3 " +
74
78
  "ORDER BY label_rank DESC, bm25_score ASC, deg DESC LIMIT ?"
75
79
  ).all(ftsQuery, lim);
76
80
  } catch (e) {
@@ -157,6 +161,149 @@ function _recordGraphTelemetry(event) {
157
161
  } catch (e) { /* non-fatal */ }
158
162
  }
159
163
 
164
+ // Re-inject graph god nodes after compaction so the LLM doesn't lose its spatial map.
165
+ function _injectCompactGraphMap() {
166
+ try {
167
+ var db = _openMonographDb();
168
+ if (!db) return;
169
+ try {
170
+ var nodeC = db.prepare("SELECT COUNT(*) AS c FROM nodes").get().c;
171
+ var gods = db.prepare(
172
+ "SELECT n.name, n.label, n.file_path, " +
173
+ "(SELECT COUNT(*) FROM edges WHERE source_id=n.id OR target_id=n.id) AS deg " +
174
+ "FROM nodes n " +
175
+ "WHERE n.label NOT IN ('Concept') AND n.file_path IS NOT NULL AND n.file_path != '' " +
176
+ "AND n.name NOT LIKE '(%' AND n.name NOT LIKE '%=>%' AND length(n.name) >= 3 " +
177
+ "ORDER BY deg DESC LIMIT 8"
178
+ ).all();
179
+ if (gods.length > 0) {
180
+ console.log('[COMPACT_GRAPH] ' + nodeC + ' nodes available. Top spatial anchors:');
181
+ for (var ci = 0; ci < gods.length; ci++) {
182
+ var g = gods[ci];
183
+ console.log(' · ' + g.name + ' [' + g.label + '] — ' + g.file_path + ' (deg ' + g.deg + ')');
184
+ }
185
+ console.log(' Use mcp__monomind__monograph_suggest first when navigating.');
186
+ }
187
+ } finally { try { db.close(); } catch (_) {} }
188
+ } catch (e) {}
189
+ }
190
+
191
+ // ── Loop drift detection ───────────────────────────────────────────────────────
192
+ // Record tool call signatures per session, warn when the same call recurs ≥3×.
193
+ function _recordToolCall(signature) {
194
+ try {
195
+ var f = path.join(CWD, '.monomind', 'metrics', 'tool-calls.json');
196
+ fs.mkdirSync(path.dirname(f), { recursive: true });
197
+ var d = {};
198
+ try { d = JSON.parse(fs.readFileSync(f, 'utf-8')); } catch (_) {}
199
+ if (typeof d !== 'object' || d === null) d = {};
200
+ // Roll over every 4 hours (new session)
201
+ if (!d.startedAt || (Date.now() - d.startedAt) > 4 * 60 * 60 * 1000) {
202
+ d = { startedAt: Date.now(), calls: {} };
203
+ }
204
+ d.calls[signature] = (d.calls[signature] || 0) + 1;
205
+ fs.writeFileSync(f, JSON.stringify(d));
206
+ return d.calls[signature];
207
+ } catch (e) { return 0; }
208
+ }
209
+
210
+ // ── Cost budget ────────────────────────────────────────────────────────────────
211
+ // Read today's cost from token-summary and compare against budget ceiling.
212
+ function _getBudgetStatus() {
213
+ try {
214
+ var budgetFile = path.join(CWD, '.monomind', 'budget.json');
215
+ var summaryFile = path.join(CWD, '.monomind', 'metrics', 'token-summary.json');
216
+ if (!fs.existsSync(summaryFile)) return null;
217
+ var summary = JSON.parse(fs.readFileSync(summaryFile, 'utf-8'));
218
+ // Support both shapes: { todayCost, monthCost } and { today: { cost }, month: { cost } }
219
+ var todayCost = summary.todayCost || (summary.today && summary.today.cost) || 0;
220
+ var monthCost = summary.monthCost || (summary.month && summary.month.cost) || 0;
221
+ var dailyLimit = 50, monthlyLimit = 1500; // defaults
222
+ try {
223
+ if (fs.existsSync(budgetFile)) {
224
+ var b = JSON.parse(fs.readFileSync(budgetFile, 'utf-8'));
225
+ dailyLimit = b.dailyLimit || dailyLimit;
226
+ monthlyLimit = b.monthlyLimit || monthlyLimit;
227
+ }
228
+ } catch (_) {}
229
+ var dailyPct = Math.round((todayCost / dailyLimit) * 100);
230
+ var monthlyPct = Math.round((monthCost / monthlyLimit) * 100);
231
+ return {
232
+ todayCost: todayCost, monthCost: monthCost,
233
+ dailyLimit: dailyLimit, monthlyLimit: monthlyLimit,
234
+ dailyPct: dailyPct, monthlyPct: monthlyPct,
235
+ alert: dailyPct >= 80 || monthlyPct >= 80,
236
+ breached: dailyPct >= 100 || monthlyPct >= 100,
237
+ };
238
+ } catch (e) { return null; }
239
+ }
240
+
241
+ // ── Test feedback (detection only — do not auto-run) ──────────────────────────
242
+ // When LLM edits a source file, find tests that import it via monograph and
243
+ // surface the list so the LLM (or user) knows what to verify.
244
+ function _findAffectedTests(filePath) {
245
+ if (!filePath) return [];
246
+ var db = _openMonographDb();
247
+ if (!db) return [];
248
+ try {
249
+ var rel = filePath;
250
+ if (filePath.indexOf(CWD) === 0) rel = filePath.slice(CWD.length + 1);
251
+ // Find tests that IMPORTS any symbol whose target file_path matches our file.
252
+ // The graph stores IMPORTS edges to symbol nodes, not file nodes — so we
253
+ // match on the target node's file_path field instead of target_id directly.
254
+ var rows = db.prepare(
255
+ "SELECT DISTINCT src.file_path FROM edges e " +
256
+ "JOIN nodes src ON e.source_id = src.id " +
257
+ "JOIN nodes tgt ON e.target_id = tgt.id " +
258
+ "WHERE e.relation IN ('IMPORTS','CALLS','DEPENDS_ON') " +
259
+ "AND (tgt.file_path = ? OR tgt.file_path = ?) " +
260
+ "AND src.file_path IS NOT NULL AND src.file_path != '' " +
261
+ "AND (src.file_path LIKE '%test%' OR src.file_path LIKE '%.spec.%' OR src.file_path LIKE '%__tests__%') " +
262
+ "AND src.file_path NOT LIKE '%.worktrees%' " +
263
+ "LIMIT 5"
264
+ ).all(filePath, rel);
265
+ return rows.map(function(r) { return r.file_path; });
266
+ } catch (e) { return []; }
267
+ finally { try { db.close(); } catch (_) {} }
268
+ }
269
+
270
+ // ── Hook latency tracking ─────────────────────────────────────────────────────
271
+ function _recordHookLatency(handlerName, durationMs) {
272
+ try {
273
+ var f = path.join(CWD, '.monomind', 'metrics', 'hook-latency.json');
274
+ fs.mkdirSync(path.dirname(f), { recursive: true });
275
+ var d = {};
276
+ try { d = JSON.parse(fs.readFileSync(f, 'utf-8')); } catch (_) {}
277
+ if (typeof d !== 'object' || d === null) d = {};
278
+ var entry = d[handlerName] || { count: 0, total: 0, max: 0 };
279
+ entry.count++;
280
+ entry.total += durationMs;
281
+ entry.max = Math.max(entry.max, durationMs);
282
+ entry.mean = Math.round(entry.total / entry.count);
283
+ d[handlerName] = entry;
284
+ d.lastUpdated = Date.now();
285
+ fs.writeFileSync(f, JSON.stringify(d));
286
+ } catch (e) {}
287
+ }
288
+
289
+ // ── Auto-ADR decision detection ───────────────────────────────────────────────
290
+ // Record sentence-level decision markers from user prompts to .monomind/decisions.jsonl
291
+ function _recordDecisionMarkers(promptText) {
292
+ if (!promptText || typeof promptText !== 'string') return;
293
+ var markers = /\b(let's go with|we (?:chose|decided|picked|will go with)|decision[:\s]|choosing|going with|prefer to|let's use)\b[^\.\n]{0,200}/gi;
294
+ var matches = promptText.match(markers);
295
+ if (!matches || matches.length === 0) return;
296
+ try {
297
+ var f = path.join(CWD, '.monomind', 'decisions.jsonl');
298
+ var entry = JSON.stringify({
299
+ ts: Date.now(),
300
+ excerpts: matches.slice(0, 3),
301
+ prompt: promptText.slice(0, 400),
302
+ });
303
+ fs.appendFileSync(f, entry + '\n');
304
+ } catch (e) {}
305
+ }
306
+
160
307
  // Auto-rebuild the monograph after N writes — graph staleness during heavy
161
308
  // editing is the main reason suggestions go cold. Triggered by post-edit.
162
309
  function _maybeRebuildMonograph() {
@@ -836,6 +983,21 @@ const handlers = {
836
983
 
837
984
  console.log(output.join('\n'));
838
985
 
986
+ // Record any decision markers in this prompt (auto-ADR pipeline).
987
+ try { _recordDecisionMarkers(prompt); } catch (e) {}
988
+
989
+ // Cost budget — emit amber/red banner when approaching limit.
990
+ try {
991
+ var budget = _getBudgetStatus();
992
+ if (budget && budget.alert) {
993
+ if (budget.breached) {
994
+ console.log('[BUDGET_BREACHED] Daily $' + budget.todayCost.toFixed(2) + '/$' + budget.dailyLimit + ' (' + budget.dailyPct + '%) · Monthly $' + budget.monthCost.toFixed(2) + '/$' + budget.monthlyLimit + ' (' + budget.monthlyPct + '%). Switch to Haiku with /model haiku or raise limits in .monomind/budget.json.');
995
+ } else {
996
+ console.log('[BUDGET_ALERT] Daily ' + budget.dailyPct + '% of $' + budget.dailyLimit + ' · Monthly ' + budget.monthlyPct + '% of $' + budget.monthlyLimit + '. Adjust .monomind/budget.json if needed.');
997
+ }
998
+ }
999
+ } catch (e) {}
1000
+
839
1001
  // Inject monograph hint for complex tasks.
840
1002
  // Source of truth is .monomind/monograph.db (SQLite). Legacy stats.json
841
1003
  // is no longer written by the build, so it is checked only as a fallback.
@@ -987,6 +1149,13 @@ const handlers = {
987
1149
  pattern = findMatch[1] || findMatch[2] || findMatch[3] || '';
988
1150
  _recordGraphTelemetry('bash_find_call');
989
1151
  }
1152
+ if (pattern && pattern.length >= 3) {
1153
+ var sigB = 'Bash-grep:' + pattern.slice(0, 60);
1154
+ var countB = _recordToolCall(sigB);
1155
+ if (countB >= 3) {
1156
+ console.log('[LOOP_DRIFT] You\'ve grepped "' + pattern.slice(0, 40) + '" ' + countB + 'x — switch to monograph_query.');
1157
+ }
1158
+ }
990
1159
  if (pattern && pattern.length >= 3) {
991
1160
  var clean = pattern.replace(/[\\^$.*+?()\[\]{}|]/g, ' ').trim();
992
1161
  if (clean.length >= 3) {
@@ -1015,6 +1184,13 @@ const handlers = {
1015
1184
  if (typeof pattern !== 'string' || pattern.length < 3) return;
1016
1185
  var clean = pattern.replace(/[\\^$.*+?()\[\]{}|]/g, ' ').trim();
1017
1186
  if (clean.length < 3) return;
1187
+
1188
+ // Loop drift detection
1189
+ var sig = (toolName || 'Search') + ':' + clean.slice(0, 60);
1190
+ var count = _recordToolCall(sig);
1191
+ if (count >= 3) {
1192
+ console.log('[LOOP_DRIFT] You\'ve searched "' + clean.slice(0, 40) + '" ' + count + 'x this session — try monograph_query or monograph_impact for a structural answer.');
1193
+ }
1018
1194
  var suggestions = getMonographSuggestions(clean, 5);
1019
1195
  if (suggestions.length === 0) return;
1020
1196
  console.log('[MONOGRAPH_HIT] Graph already knows ' + suggestions.length + ' file(s) matching "' + clean.slice(0, 40) + '":');
@@ -1064,6 +1240,22 @@ const handlers = {
1064
1240
  }
1065
1241
  // Increment write counter and rebuild monograph when threshold hit.
1066
1242
  _maybeRebuildMonograph();
1243
+
1244
+ // Test feedback (detection-only): when editing a source file, list tests
1245
+ // that import it so the LLM/user knows what to verify next.
1246
+ try {
1247
+ var editedFile = hookInput.file_path || toolInput.file_path
1248
+ || process.env.TOOL_INPUT_file_path || args[0] || '';
1249
+ if (editedFile && !editedFile.match(/\.(test|spec)\./) && !editedFile.includes('__tests__')) {
1250
+ var affectedTests = _findAffectedTests(editedFile);
1251
+ if (affectedTests.length > 0) {
1252
+ console.log('[AFFECTED_TESTS] ' + affectedTests.length + ' test(s) cover this file:');
1253
+ for (var ti = 0; ti < Math.min(5, affectedTests.length); ti++) {
1254
+ console.log(' · ' + affectedTests[ti]);
1255
+ }
1256
+ }
1257
+ }
1258
+ } catch (e) {}
1067
1259
  // ── Security-Sensitive File Auto-Alert ────────────────────────────────
1068
1260
  // When editing auth, security, crypto, or env-related files, flag it
1069
1261
  try {
@@ -1919,11 +2111,9 @@ const handlers = {
1919
2111
  },
1920
2112
 
1921
2113
  'compact-manual': async () => {
1922
- // Consolidate intelligence before compaction so patterns survive
1923
2114
  if (intelligence && intelligence.consolidate) {
1924
2115
  try { await runWithTimeout(function() { return intelligence.consolidate(); }, 'intelligence.consolidate()'); } catch (e) { /* non-fatal */ }
1925
2116
  }
1926
- // Save current routing context for post-compact restore
1927
2117
  try {
1928
2118
  var lastRoute = path.join(CWD, '.monomind', 'last-route.json');
1929
2119
  if (fs.existsSync(lastRoute)) {
@@ -1931,11 +2121,11 @@ const handlers = {
1931
2121
  console.log('[COMPACT_CONTEXT] Last route: ' + route.agent + ' (' + (route.confidence != null ? (route.confidence * 100).toFixed(0) : '?') + '%)');
1932
2122
  }
1933
2123
  } catch (e) { /* non-fatal */ }
2124
+ _injectCompactGraphMap();
1934
2125
  console.log('[COMPACT] Manual compaction — intelligence consolidated, context preserved');
1935
2126
  },
1936
2127
 
1937
2128
  'compact-auto': async () => {
1938
- // Same consolidation for auto-compact
1939
2129
  if (intelligence && intelligence.consolidate) {
1940
2130
  try { await runWithTimeout(function() { return intelligence.consolidate(); }, 'intelligence.consolidate()'); } catch (e) { /* non-fatal */ }
1941
2131
  }
@@ -1946,6 +2136,7 @@ const handlers = {
1946
2136
  console.log('[COMPACT_CONTEXT] Last route: ' + route.agent + ' (' + (route.confidence != null ? (route.confidence * 100).toFixed(0) : '?') + '%)');
1947
2137
  }
1948
2138
  } catch (e) { /* non-fatal */ }
2139
+ _injectCompactGraphMap();
1949
2140
  console.log('[COMPACT] Auto compaction — intelligence consolidated, context preserved');
1950
2141
  console.log('GOLDEN RULE: 1 message = all parallel operations');
1951
2142
  },
@@ -2038,6 +2229,62 @@ const handlers = {
2038
2229
  console.log('[OK] Agent registered');
2039
2230
  },
2040
2231
 
2232
+ // Draft an ADR from accumulated decision markers in .monomind/decisions.jsonl.
2233
+ // Usage: node hook-handler.cjs adr-draft (or via /adr slash command)
2234
+ 'adr-draft': () => {
2235
+ var jsonl = path.join(CWD, '.monomind', 'decisions.jsonl');
2236
+ if (!fs.existsSync(jsonl)) {
2237
+ console.log('[ADR] No decisions recorded yet. Type prompts containing markers like "let\'s go with X", "we chose Y", "decision: Z" to populate the log.');
2238
+ return;
2239
+ }
2240
+ var lines = fs.readFileSync(jsonl, 'utf-8').trim().split('\n').filter(Boolean);
2241
+ if (lines.length === 0) {
2242
+ console.log('[ADR] decisions.jsonl is empty.');
2243
+ return;
2244
+ }
2245
+ // Group decisions captured in the last 7 days
2246
+ var cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
2247
+ var recent = lines.map(function(l) { try { return JSON.parse(l); } catch (_) { return null; } })
2248
+ .filter(function(d) { return d && d.ts >= cutoff; });
2249
+ if (recent.length === 0) {
2250
+ console.log('[ADR] No decisions in the last 7 days. Older entries: ' + lines.length + '.');
2251
+ return;
2252
+ }
2253
+
2254
+ var adrsDir = path.join(CWD, 'docs', 'adrs');
2255
+ try { fs.mkdirSync(adrsDir, { recursive: true }); } catch (_) {}
2256
+ // Pick next ADR number
2257
+ var existing = [];
2258
+ try { existing = fs.readdirSync(adrsDir).filter(function(f) { return /^ADR-\d{4}/.test(f); }); } catch (_) {}
2259
+ var nextNum = existing.length + 1;
2260
+ var num = String(nextNum).padStart(4, '0');
2261
+ var stamp = new Date().toISOString().slice(0,10);
2262
+ var slug = 'session-decisions';
2263
+ var fname = 'ADR-' + num + '-' + stamp + '-' + slug + '.md';
2264
+ var outPath = path.join(adrsDir, fname);
2265
+
2266
+ var body = '# ADR-' + num + ': Session decisions (' + stamp + ')\n\n' +
2267
+ '**Status:** Proposed\n**Date:** ' + stamp + '\n\n' +
2268
+ '## Context\n\n' +
2269
+ 'During recent sessions, the following decision markers were captured ' +
2270
+ 'from user prompts. Each excerpt is the surrounding sentence at the time.\n\n' +
2271
+ '## Decisions\n\n';
2272
+ for (var i = 0; i < recent.length; i++) {
2273
+ var d = recent[i];
2274
+ var date = new Date(d.ts).toISOString().slice(0,16).replace('T',' ');
2275
+ body += '### ' + (i + 1) + '. ' + date + '\n\n';
2276
+ for (var j = 0; j < d.excerpts.length; j++) {
2277
+ body += '> ' + d.excerpts[j].trim() + '\n\n';
2278
+ }
2279
+ if (d.prompt) body += '_Prompt:_ ' + d.prompt.slice(0, 200) + (d.prompt.length > 200 ? '…' : '') + '\n\n';
2280
+ }
2281
+ body += '## Consequences\n\n_(fill in after review)_\n\n' +
2282
+ '## Status\n\nProposed — awaiting human review and refinement.\n';
2283
+ fs.writeFileSync(outPath, body);
2284
+ console.log('[ADR_DRAFT] Wrote ' + recent.length + ' decision(s) to ' + outPath);
2285
+ console.log(' Edit the file to fill in Context and Consequences, then change Status to Accepted/Rejected.');
2286
+ },
2287
+
2041
2288
  'status': () => {
2042
2289
  console.log('[OK] Status check');
2043
2290
  },
@@ -2052,15 +2299,18 @@ const handlers = {
2052
2299
  };
2053
2300
 
2054
2301
  if (command && handlers[command]) {
2302
+ var _hookStart = Date.now();
2055
2303
  try {
2056
2304
  await Promise.resolve(handlers[command]());
2057
2305
  } catch (e) {
2058
2306
  console.log('[WARN] Hook ' + command + ' encountered an error: ' + e.message);
2307
+ } finally {
2308
+ try { _recordHookLatency(command, Date.now() - _hookStart); } catch (_) {}
2059
2309
  }
2060
2310
  } else if (command) {
2061
2311
  console.log('[OK] Hook: ' + command);
2062
2312
  } else {
2063
- console.log('Usage: hook-handler.cjs <route|pre-bash|post-edit|session-restore|session-end|pre-task|post-task|compact-manual|compact-auto|status|stats>');
2313
+ console.log('Usage: hook-handler.cjs <route|pre-bash|pre-search|post-edit|post-read|post-graph-tool|session-restore|session-end|pre-task|post-task|compact-manual|compact-auto|status|stats>');
2064
2314
  }
2065
2315
  }
2066
2316
 
@@ -696,6 +696,30 @@ function getGraphifyStats() {
696
696
  return { nodes: 0, edges: 0, exists: false };
697
697
  }
698
698
 
699
+ // Hook latency — sum of mean times for hooks that fire on every prompt.
700
+ function getHookLatency() {
701
+ const p = path.join(CWD, '.monomind', 'metrics', 'hook-latency.json');
702
+ try {
703
+ if (!fs.existsSync(p)) return null;
704
+ const d = JSON.parse(fs.readFileSync(p, 'utf-8'));
705
+ // Hooks that run per-prompt: route, pre-search, post-read, post-edit
706
+ const perPrompt = ['route'];
707
+ let totalMs = 0;
708
+ let count = 0;
709
+ for (const h of perPrompt) {
710
+ if (d[h] && d[h].mean) { totalMs += d[h].mean; count++; }
711
+ }
712
+ if (count === 0) return null;
713
+ // Find slowest hook
714
+ let slowest = null;
715
+ for (const k of Object.keys(d)) {
716
+ if (k === 'lastUpdated' || !d[k] || typeof d[k] !== 'object') continue;
717
+ if (!slowest || d[k].mean > slowest.mean) slowest = { name: k, mean: d[k].mean };
718
+ }
719
+ return { perPromptMs: totalMs, slowest };
720
+ } catch { return null; }
721
+ }
722
+
699
723
  // Graph usage telemetry — ratio of monograph_* vs Grep/Glob, plus $ saved
700
724
  function getGraphUsage() {
701
725
  const usagePath = path.join(CWD, '.monomind', 'metrics', 'graph-usage.json');
@@ -1270,7 +1294,16 @@ function generateDashboard() {
1270
1294
  usageStr = ` ${DIV} ${col}📊 graph ${usage.pct}%${x.reset}${x.slate} · grep ${100 - usage.pct}%${x.reset}${saved}`;
1271
1295
  }
1272
1296
 
1273
- lines.push(`${x.purple}🤖 AGENT${x.reset} ${agentStr} ${DIV} ${loopStr}${usageStr}`);
1297
+ // Hook latency — surface when slow (>500ms per prompt)
1298
+ const lat = getHookLatency();
1299
+ let latStr = '';
1300
+ if (lat && lat.perPromptMs > 500) {
1301
+ latStr = ` ${DIV} ${x.coral}⚡ hooks ${lat.perPromptMs}ms${x.reset}`;
1302
+ } else if (lat && lat.perPromptMs > 0) {
1303
+ latStr = ` ${DIV} ${x.dim}⚡ ${lat.perPromptMs}ms${x.reset}`;
1304
+ }
1305
+
1306
+ lines.push(`${x.purple}🤖 AGENT${x.reset} ${agentStr} ${DIV} ${loopStr}${usageStr}${latStr}`);
1274
1307
  lines.push(SEP);
1275
1308
 
1276
1309
  // ── Row 2: Graph freshness + Pending HIL ─────────────────────
@@ -1 +1 @@
1
- {"version":3,"file":"statusline-generator.d.ts","sourceRoot":"","sources":["../../../src/init/statusline-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CA8oCrE;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CA8BnE"}
1
+ {"version":3,"file":"statusline-generator.d.ts","sourceRoot":"","sources":["../../../src/init/statusline-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CA4qCrE;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CA8BnE"}
@@ -828,6 +828,27 @@ function getTriggerStats() {
828
828
  } catch { return { triggers: 0, agents: 0 }; }
829
829
  }
830
830
 
831
+ // Hook latency — surface slow per-prompt hooks in the statusline.
832
+ function getHookLatency() {
833
+ const p = path.join(CWD, '.monomind', 'metrics', 'hook-latency.json');
834
+ try {
835
+ if (!fs.existsSync(p)) return null;
836
+ const d = JSON.parse(fs.readFileSync(p, 'utf-8'));
837
+ const perPrompt = ['route'];
838
+ let totalMs = 0; let count = 0;
839
+ for (const h of perPrompt) {
840
+ if (d[h] && d[h].mean) { totalMs += d[h].mean; count++; }
841
+ }
842
+ if (count === 0) return null;
843
+ let slowest = null;
844
+ for (const k of Object.keys(d)) {
845
+ if (k === 'lastUpdated' || !d[k] || typeof d[k] !== 'object') continue;
846
+ if (!slowest || d[k].mean > slowest.mean) slowest = { name: k, mean: d[k].mean };
847
+ }
848
+ return { perPromptMs: totalMs, slowest: slowest };
849
+ } catch { return null; }
850
+ }
851
+
831
852
  // Graph usage telemetry — ratio of monograph_* vs Grep/Glob/Bash-grep, + $ saved
832
853
  function getGraphUsage() {
833
854
  const usagePath = path.join(CWD, '.monomind', 'metrics', 'graph-usage.json');
@@ -1096,7 +1117,16 @@ function generateDashboard() {
1096
1117
  usageStr = \` \${DIV} \${col}📊 graph \${usage.pct}%\${x.reset}\${x.slate} · grep \${100 - usage.pct}%\${x.reset}\${saved}\`;
1097
1118
  }
1098
1119
 
1099
- lines.push(\`\${x.purple}🤖 AGENT\${x.reset} \${agentStr} \${DIV} \${loopStr}\${usageStr}\`);
1120
+ // Hook latency — surface when slow (>500ms per prompt)
1121
+ const lat = getHookLatency();
1122
+ let latStr = '';
1123
+ if (lat && lat.perPromptMs > 500) {
1124
+ latStr = \` \${DIV} \${x.coral}⚡ hooks \${lat.perPromptMs}ms\${x.reset}\`;
1125
+ } else if (lat && lat.perPromptMs > 0) {
1126
+ latStr = \` \${DIV} \${x.dim}⚡ \${lat.perPromptMs}ms\${x.reset}\`;
1127
+ }
1128
+
1129
+ lines.push(\`\${x.purple}🤖 AGENT\${x.reset} \${agentStr} \${DIV} \${loopStr}\${usageStr}\${latStr}\`);
1100
1130
  lines.push(SEP);
1101
1131
 
1102
1132
  // ── Row 2: Graph freshness + Pending HIL ─────────────────────
@@ -1 +1 @@
1
- {"version":3,"file":"statusline-generator.js","sourceRoot":"","sources":["../../../src/init/statusline-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH;;;;;;;;;GASG;AACH,MAAM,UAAU,wBAAwB,CAAC,OAAoB;IAC3D,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC;IAC5C,OAAO;;;;;;;;;;;;;;;;;;;;;;;eAuBM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAonCvB,CAAC;AACF,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,OAAoB;IACzD,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;QAChC,OAAO,sCAAsC,CAAC;IAChD,CAAC;IAED,OAAO;;;;;;;;;;;;;;;;;;;;;;;;CAwBR,CAAC;AACF,CAAC"}
1
+ {"version":3,"file":"statusline-generator.js","sourceRoot":"","sources":["../../../src/init/statusline-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH;;;;;;;;;GASG;AACH,MAAM,UAAU,wBAAwB,CAAC,OAAoB;IAC3D,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC;IAC5C,OAAO;;;;;;;;;;;;;;;;;;;;;;;eAuBM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkpCvB,CAAC;AACF,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,OAAoB;IACzD,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;QAChC,OAAO,sCAAsC,CAAC;IAChD,CAAC;IAED,OAAO;;;;;;;;;;;;;;;;;;;;;;;;CAwBR,CAAC;AACF,CAAC"}
@@ -2128,6 +2128,70 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
2128
2128
  return;
2129
2129
  }
2130
2130
 
2131
+ // ------------------------------------------------- Monograph
2132
+ // GET /api/monograph — node/edge counts, top god nodes, type distribution.
2133
+ // (Distinct from /api/graph which serves session/journal graph data.)
2134
+ // Reads .monomind/monograph.db via sqlite3 CLI to avoid bundling better-sqlite3.
2135
+ if (req.method === 'GET' && url === '/api/monograph') {
2136
+ try {
2137
+ const dbPath = path.join(projectDir || process.cwd(), '.monomind', 'monograph.db');
2138
+ if (!fs.existsSync(dbPath)) {
2139
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2140
+ res.end(JSON.stringify({ exists: false }));
2141
+ return;
2142
+ }
2143
+ const { execSync } = await import('child_process');
2144
+ // Pipe SQL via stdin to avoid shell quoting issues with single-quoted SQL strings.
2145
+ const runSql = (sql, timeout = 5000) => {
2146
+ try {
2147
+ return execSync(`sqlite3 -json "${dbPath}"`,
2148
+ { encoding: 'utf-8', timeout: timeout, input: sql + ';' });
2149
+ } catch (e) { return '[]'; }
2150
+ };
2151
+ const counts = JSON.parse(runSql(
2152
+ "SELECT (SELECT COUNT(*) FROM nodes) AS nodes, (SELECT COUNT(*) FROM edges) AS edges;"
2153
+ ) || '[{}]')[0] || { nodes: 0, edges: 0 };
2154
+ // Compute degree in one pass via GROUP BY (much faster than per-row subquery).
2155
+ const gods = JSON.parse(runSql(
2156
+ "WITH deg(node_id, d) AS (" +
2157
+ " SELECT source_id, COUNT(*) FROM edges GROUP BY source_id " +
2158
+ " UNION ALL " +
2159
+ " SELECT target_id, COUNT(*) FROM edges GROUP BY target_id" +
2160
+ "), totals AS (" +
2161
+ " SELECT node_id, SUM(d) AS deg FROM deg GROUP BY node_id" +
2162
+ ") " +
2163
+ "SELECT n.name, n.label, n.file_path, t.deg " +
2164
+ "FROM nodes n JOIN totals t ON t.node_id = n.id " +
2165
+ "WHERE n.label NOT IN ('Concept') " +
2166
+ "AND n.file_path IS NOT NULL AND n.file_path != '' " +
2167
+ "AND n.name NOT LIKE '(%' AND length(n.name) >= 3 " +
2168
+ "ORDER BY t.deg DESC LIMIT 20",
2169
+ 10000
2170
+ ) || '[]');
2171
+ const types = JSON.parse(runSql(
2172
+ "SELECT label, COUNT(*) AS count FROM nodes GROUP BY label ORDER BY count DESC LIMIT 12"
2173
+ ) || '[]');
2174
+ const relations = JSON.parse(runSql(
2175
+ "SELECT relation, COUNT(*) AS count FROM edges GROUP BY relation ORDER BY count DESC"
2176
+ ) || '[]');
2177
+
2178
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2179
+ res.end(JSON.stringify({
2180
+ exists: true,
2181
+ nodes: counts.nodes,
2182
+ edges: counts.edges,
2183
+ godNodes: gods,
2184
+ typeDistribution: types,
2185
+ relationDistribution: relations,
2186
+ updatedAt: fs.statSync(dbPath).mtime,
2187
+ }));
2188
+ } catch (err) {
2189
+ res.writeHead(500, { 'Content-Type': 'application/json' });
2190
+ res.end(JSON.stringify({ error: String(err) }));
2191
+ }
2192
+ return;
2193
+ }
2194
+
2131
2195
  // ------------------------------------------------- Org management
2132
2196
  // GET /api/orgs — list all saved org configs
2133
2197
  if (req.method === 'GET' && url === '/api/orgs') {