@monoes/monomindcli 1.10.9 → 1.10.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/helpers/hook-handler.cjs +318 -5
- package/.claude/helpers/statusline.cjs +43 -5
- package/dist/src/init/statusline-generator.d.ts.map +1 -1
- package/dist/src/init/statusline-generator.js +40 -6
- package/dist/src/init/statusline-generator.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -45,23 +45,33 @@ function getMonographSuggestions(taskText, limit) {
|
|
|
45
45
|
if (!db) return [];
|
|
46
46
|
try {
|
|
47
47
|
var words = String(taskText).toLowerCase().match(/[a-z][a-z0-9_-]{3,}/g) || [];
|
|
48
|
-
var stop = { "this":1,"that":1,"with":1,"from":1,"have":1,"into":1,"their":1,"what":1,"when":1,"where":1,"which":1,"should":1,"would":1,"could":1,"make":1,"just":1,"also":1,"them":1,"they":1,"will":1,"been":1,"were":1,"because":1,"about":1,"does":1,"work":1 };
|
|
48
|
+
var stop = { "this":1,"that":1,"with":1,"from":1,"have":1,"into":1,"their":1,"what":1,"when":1,"where":1,"which":1,"should":1,"would":1,"could":1,"make":1,"just":1,"also":1,"them":1,"they":1,"will":1,"been":1,"were":1,"because":1,"about":1,"does":1,"work":1,"else":1,"more":1,"some":1,"like":1,"need":1,"want":1,"used":1,"using":1,"please":1,"thanks":1,"good":1,"great":1,"nice":1,"thing":1,"things":1,"better":1,"again":1,"first":1,"then":1,"only":1,"even":1 };
|
|
49
49
|
var uniq = {};
|
|
50
50
|
for (var i = 0; i < words.length; i++) if (!stop[words[i]]) uniq[words[i]] = 1;
|
|
51
51
|
var keys = Object.keys(uniq).slice(0, 8);
|
|
52
|
+
// Smart filter: free-form prompts need ≥2 content words to avoid noise.
|
|
53
|
+
// Single-word inputs are allowed only when they look like a symbol/search
|
|
54
|
+
// (entire string is ≤30 chars and contains letters+separators only).
|
|
55
|
+
var isSymbolLookup = taskText.length <= 30 && /^[a-zA-Z0-9_\-./:]+$/.test(taskText.trim());
|
|
52
56
|
if (keys.length === 0) return [];
|
|
57
|
+
if (keys.length < 2 && !isSymbolLookup) return [];
|
|
53
58
|
|
|
54
59
|
var ftsQuery = keys.map(function(k){ return '"' + k.replace(/"/g, "") + '"'; }).join(" OR ");
|
|
55
60
|
var lim = Math.max(1, limit || 5);
|
|
56
61
|
var rows = [];
|
|
57
62
|
try {
|
|
63
|
+
// BM25 ranks better than degree for keyword relevance; tie-break by deg.
|
|
64
|
+
// File/Function/Class outrank Section so navigable nodes win.
|
|
58
65
|
rows = db.prepare(
|
|
59
66
|
"SELECT n.id, n.name, n.label, n.file_path AS file, " +
|
|
60
|
-
"(
|
|
67
|
+
"bm25(nodes_fts) AS bm25_score, " +
|
|
68
|
+
"(SELECT COUNT(*) FROM edges WHERE source_id=n.id OR target_id=n.id) AS deg, " +
|
|
69
|
+
"CASE n.label WHEN 'File' THEN 3 WHEN 'Function' THEN 3 WHEN 'Class' THEN 3 " +
|
|
70
|
+
" WHEN 'Method' THEN 2 WHEN 'Interface' THEN 2 ELSE 1 END AS label_rank " +
|
|
61
71
|
"FROM nodes_fts f JOIN nodes n ON f.rowid = n.rowid " +
|
|
62
72
|
"WHERE nodes_fts MATCH ? AND n.file_path IS NOT NULL AND n.file_path != '' " +
|
|
63
73
|
"AND n.label NOT IN ('Concept') " +
|
|
64
|
-
"ORDER BY deg DESC LIMIT ?"
|
|
74
|
+
"ORDER BY label_rank DESC, bm25_score ASC, deg DESC LIMIT ?"
|
|
65
75
|
).all(ftsQuery, lim);
|
|
66
76
|
} catch (e) {
|
|
67
77
|
var likeFrag = keys.map(function(){ return "lower(n.name) LIKE ?"; }).join(" OR ");
|
|
@@ -108,6 +118,17 @@ function getMonographNeighbors(filePath) {
|
|
|
108
118
|
finally { try { db.close(); } catch (_) {} }
|
|
109
119
|
}
|
|
110
120
|
|
|
121
|
+
// Rough per-event token + USD cost estimates. Tuned to Sonnet input pricing
|
|
122
|
+
// ($3/M tokens) — adjust if needed. Used by the statusline to surface savings.
|
|
123
|
+
var _TOKEN_PER_EVENT = {
|
|
124
|
+
monograph_call: 300, // typical monograph_query result size
|
|
125
|
+
grep_call: 2000, // typical Grep tool output across many files
|
|
126
|
+
glob_call: 800,
|
|
127
|
+
bash_grep_call: 2000,
|
|
128
|
+
bash_find_call: 800,
|
|
129
|
+
};
|
|
130
|
+
var _DOLLAR_PER_1M_TOKENS = 3.0;
|
|
131
|
+
|
|
111
132
|
function _recordGraphTelemetry(event) {
|
|
112
133
|
try {
|
|
113
134
|
var metricsDir = path.join(CWD, ".monomind", "metrics");
|
|
@@ -117,11 +138,181 @@ function _recordGraphTelemetry(event) {
|
|
|
117
138
|
try { d = JSON.parse(fs.readFileSync(f, "utf-8")); } catch (e) {}
|
|
118
139
|
if (typeof d !== "object" || d === null) d = {};
|
|
119
140
|
d[event] = (d[event] || 0) + 1;
|
|
141
|
+
|
|
142
|
+
// Token-saved estimator: each monograph_call avoids a grep equivalent
|
|
143
|
+
// (~2000 tokens) at a cost of ~300 tokens — net ~1700 saved.
|
|
144
|
+
if (event === 'monograph_call') {
|
|
145
|
+
var saved = (_TOKEN_PER_EVENT.grep_call - _TOKEN_PER_EVENT.monograph_call);
|
|
146
|
+
d.tokens_saved = (d.tokens_saved || 0) + saved;
|
|
147
|
+
d.dollars_saved = ((d.tokens_saved / 1000000) * _DOLLAR_PER_1M_TOKENS);
|
|
148
|
+
}
|
|
149
|
+
// Each unprompted grep/bash_grep "wastes" the same amount vs the graph alternative.
|
|
150
|
+
if (event === 'grep_call' || event === 'bash_grep_call') {
|
|
151
|
+
var wasted = (_TOKEN_PER_EVENT.grep_call - _TOKEN_PER_EVENT.monograph_call);
|
|
152
|
+
d.tokens_wasted = (d.tokens_wasted || 0) + wasted;
|
|
153
|
+
}
|
|
154
|
+
|
|
120
155
|
d.lastUpdated = Date.now();
|
|
121
156
|
fs.writeFileSync(f, JSON.stringify(d));
|
|
122
157
|
} catch (e) { /* non-fatal */ }
|
|
123
158
|
}
|
|
124
159
|
|
|
160
|
+
// ── Loop drift detection ───────────────────────────────────────────────────────
|
|
161
|
+
// Record tool call signatures per session, warn when the same call recurs ≥3×.
|
|
162
|
+
function _recordToolCall(signature) {
|
|
163
|
+
try {
|
|
164
|
+
var f = path.join(CWD, '.monomind', 'metrics', 'tool-calls.json');
|
|
165
|
+
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
166
|
+
var d = {};
|
|
167
|
+
try { d = JSON.parse(fs.readFileSync(f, 'utf-8')); } catch (_) {}
|
|
168
|
+
if (typeof d !== 'object' || d === null) d = {};
|
|
169
|
+
// Roll over every 4 hours (new session)
|
|
170
|
+
if (!d.startedAt || (Date.now() - d.startedAt) > 4 * 60 * 60 * 1000) {
|
|
171
|
+
d = { startedAt: Date.now(), calls: {} };
|
|
172
|
+
}
|
|
173
|
+
d.calls[signature] = (d.calls[signature] || 0) + 1;
|
|
174
|
+
fs.writeFileSync(f, JSON.stringify(d));
|
|
175
|
+
return d.calls[signature];
|
|
176
|
+
} catch (e) { return 0; }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Cost budget ────────────────────────────────────────────────────────────────
|
|
180
|
+
// Read today's cost from token-summary and compare against budget ceiling.
|
|
181
|
+
function _getBudgetStatus() {
|
|
182
|
+
try {
|
|
183
|
+
var budgetFile = path.join(CWD, '.monomind', 'budget.json');
|
|
184
|
+
var summaryFile = path.join(CWD, '.monomind', 'metrics', 'token-summary.json');
|
|
185
|
+
if (!fs.existsSync(summaryFile)) return null;
|
|
186
|
+
var summary = JSON.parse(fs.readFileSync(summaryFile, 'utf-8'));
|
|
187
|
+
// Support both shapes: { todayCost, monthCost } and { today: { cost }, month: { cost } }
|
|
188
|
+
var todayCost = summary.todayCost || (summary.today && summary.today.cost) || 0;
|
|
189
|
+
var monthCost = summary.monthCost || (summary.month && summary.month.cost) || 0;
|
|
190
|
+
var dailyLimit = 50, monthlyLimit = 1500; // defaults
|
|
191
|
+
try {
|
|
192
|
+
if (fs.existsSync(budgetFile)) {
|
|
193
|
+
var b = JSON.parse(fs.readFileSync(budgetFile, 'utf-8'));
|
|
194
|
+
dailyLimit = b.dailyLimit || dailyLimit;
|
|
195
|
+
monthlyLimit = b.monthlyLimit || monthlyLimit;
|
|
196
|
+
}
|
|
197
|
+
} catch (_) {}
|
|
198
|
+
var dailyPct = Math.round((todayCost / dailyLimit) * 100);
|
|
199
|
+
var monthlyPct = Math.round((monthCost / monthlyLimit) * 100);
|
|
200
|
+
return {
|
|
201
|
+
todayCost: todayCost, monthCost: monthCost,
|
|
202
|
+
dailyLimit: dailyLimit, monthlyLimit: monthlyLimit,
|
|
203
|
+
dailyPct: dailyPct, monthlyPct: monthlyPct,
|
|
204
|
+
alert: dailyPct >= 80 || monthlyPct >= 80,
|
|
205
|
+
breached: dailyPct >= 100 || monthlyPct >= 100,
|
|
206
|
+
};
|
|
207
|
+
} catch (e) { return null; }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Test feedback (detection only — do not auto-run) ──────────────────────────
|
|
211
|
+
// When LLM edits a source file, find tests that import it via monograph and
|
|
212
|
+
// surface the list so the LLM (or user) knows what to verify.
|
|
213
|
+
function _findAffectedTests(filePath) {
|
|
214
|
+
if (!filePath) return [];
|
|
215
|
+
var db = _openMonographDb();
|
|
216
|
+
if (!db) return [];
|
|
217
|
+
try {
|
|
218
|
+
var rel = filePath;
|
|
219
|
+
if (filePath.indexOf(CWD) === 0) rel = filePath.slice(CWD.length + 1);
|
|
220
|
+
// Find tests that IMPORTS any symbol whose target file_path matches our file.
|
|
221
|
+
// The graph stores IMPORTS edges to symbol nodes, not file nodes — so we
|
|
222
|
+
// match on the target node's file_path field instead of target_id directly.
|
|
223
|
+
var rows = db.prepare(
|
|
224
|
+
"SELECT DISTINCT src.file_path FROM edges e " +
|
|
225
|
+
"JOIN nodes src ON e.source_id = src.id " +
|
|
226
|
+
"JOIN nodes tgt ON e.target_id = tgt.id " +
|
|
227
|
+
"WHERE e.relation IN ('IMPORTS','CALLS','DEPENDS_ON') " +
|
|
228
|
+
"AND (tgt.file_path = ? OR tgt.file_path = ?) " +
|
|
229
|
+
"AND src.file_path IS NOT NULL AND src.file_path != '' " +
|
|
230
|
+
"AND (src.file_path LIKE '%test%' OR src.file_path LIKE '%.spec.%' OR src.file_path LIKE '%__tests__%') " +
|
|
231
|
+
"AND src.file_path NOT LIKE '%.worktrees%' " +
|
|
232
|
+
"LIMIT 5"
|
|
233
|
+
).all(filePath, rel);
|
|
234
|
+
return rows.map(function(r) { return r.file_path; });
|
|
235
|
+
} catch (e) { return []; }
|
|
236
|
+
finally { try { db.close(); } catch (_) {} }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── Hook latency tracking ─────────────────────────────────────────────────────
|
|
240
|
+
function _recordHookLatency(handlerName, durationMs) {
|
|
241
|
+
try {
|
|
242
|
+
var f = path.join(CWD, '.monomind', 'metrics', 'hook-latency.json');
|
|
243
|
+
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
244
|
+
var d = {};
|
|
245
|
+
try { d = JSON.parse(fs.readFileSync(f, 'utf-8')); } catch (_) {}
|
|
246
|
+
if (typeof d !== 'object' || d === null) d = {};
|
|
247
|
+
var entry = d[handlerName] || { count: 0, total: 0, max: 0 };
|
|
248
|
+
entry.count++;
|
|
249
|
+
entry.total += durationMs;
|
|
250
|
+
entry.max = Math.max(entry.max, durationMs);
|
|
251
|
+
entry.mean = Math.round(entry.total / entry.count);
|
|
252
|
+
d[handlerName] = entry;
|
|
253
|
+
d.lastUpdated = Date.now();
|
|
254
|
+
fs.writeFileSync(f, JSON.stringify(d));
|
|
255
|
+
} catch (e) {}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ── Auto-ADR decision detection ───────────────────────────────────────────────
|
|
259
|
+
// Record sentence-level decision markers from user prompts to .monomind/decisions.jsonl
|
|
260
|
+
function _recordDecisionMarkers(promptText) {
|
|
261
|
+
if (!promptText || typeof promptText !== 'string') return;
|
|
262
|
+
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;
|
|
263
|
+
var matches = promptText.match(markers);
|
|
264
|
+
if (!matches || matches.length === 0) return;
|
|
265
|
+
try {
|
|
266
|
+
var f = path.join(CWD, '.monomind', 'decisions.jsonl');
|
|
267
|
+
var entry = JSON.stringify({
|
|
268
|
+
ts: Date.now(),
|
|
269
|
+
excerpts: matches.slice(0, 3),
|
|
270
|
+
prompt: promptText.slice(0, 400),
|
|
271
|
+
});
|
|
272
|
+
fs.appendFileSync(f, entry + '\n');
|
|
273
|
+
} catch (e) {}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Auto-rebuild the monograph after N writes — graph staleness during heavy
|
|
277
|
+
// editing is the main reason suggestions go cold. Triggered by post-edit.
|
|
278
|
+
function _maybeRebuildMonograph() {
|
|
279
|
+
try {
|
|
280
|
+
var metricsDir = path.join(CWD, ".monomind", "metrics");
|
|
281
|
+
fs.mkdirSync(metricsDir, { recursive: true });
|
|
282
|
+
var f = path.join(metricsDir, "graph-rebuild.json");
|
|
283
|
+
var d = {};
|
|
284
|
+
try { d = JSON.parse(fs.readFileSync(f, "utf-8")); } catch (_) {}
|
|
285
|
+
if (typeof d !== "object" || d === null) d = {};
|
|
286
|
+
d.writesSinceRebuild = (d.writesSinceRebuild || 0) + 1;
|
|
287
|
+
d.lastWriteAt = Date.now();
|
|
288
|
+
var THRESHOLD = 20;
|
|
289
|
+
var MIN_INTERVAL_MS = 5 * 60 * 1000; // never more often than every 5 min
|
|
290
|
+
var dueByCount = d.writesSinceRebuild >= THRESHOLD;
|
|
291
|
+
var dueByTime = !d.lastRebuildAt || (Date.now() - d.lastRebuildAt) > MIN_INTERVAL_MS;
|
|
292
|
+
if (dueByCount && dueByTime) {
|
|
293
|
+
// Reset counter immediately so concurrent post-edits don't all fire.
|
|
294
|
+
d.writesSinceRebuild = 0;
|
|
295
|
+
d.lastRebuildAt = Date.now();
|
|
296
|
+
fs.writeFileSync(f, JSON.stringify(d));
|
|
297
|
+
// Fire-and-forget background freshen script (same one used by SessionStart).
|
|
298
|
+
try {
|
|
299
|
+
var freshenScript = path.join(CWD, '.claude', 'helpers', 'graphify-freshen.cjs');
|
|
300
|
+
if (fs.existsSync(freshenScript)) {
|
|
301
|
+
var spawn = require('child_process').spawn;
|
|
302
|
+
var child = spawn(process.execPath, [freshenScript], {
|
|
303
|
+
detached: true,
|
|
304
|
+
stdio: 'ignore',
|
|
305
|
+
cwd: CWD,
|
|
306
|
+
});
|
|
307
|
+
child.unref();
|
|
308
|
+
}
|
|
309
|
+
} catch (_) {}
|
|
310
|
+
} else {
|
|
311
|
+
fs.writeFileSync(f, JSON.stringify(d));
|
|
312
|
+
}
|
|
313
|
+
} catch (e) { /* non-fatal */ }
|
|
314
|
+
}
|
|
315
|
+
|
|
125
316
|
function safeRequire(modulePath) {
|
|
126
317
|
try {
|
|
127
318
|
if (fs.existsSync(modulePath)) {
|
|
@@ -761,6 +952,21 @@ const handlers = {
|
|
|
761
952
|
|
|
762
953
|
console.log(output.join('\n'));
|
|
763
954
|
|
|
955
|
+
// Record any decision markers in this prompt (auto-ADR pipeline).
|
|
956
|
+
try { _recordDecisionMarkers(prompt); } catch (e) {}
|
|
957
|
+
|
|
958
|
+
// Cost budget — emit amber/red banner when approaching limit.
|
|
959
|
+
try {
|
|
960
|
+
var budget = _getBudgetStatus();
|
|
961
|
+
if (budget && budget.alert) {
|
|
962
|
+
if (budget.breached) {
|
|
963
|
+
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.');
|
|
964
|
+
} else {
|
|
965
|
+
console.log('[BUDGET_ALERT] Daily ' + budget.dailyPct + '% of $' + budget.dailyLimit + ' · Monthly ' + budget.monthlyPct + '% of $' + budget.monthlyLimit + '. Adjust .monomind/budget.json if needed.');
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
} catch (e) {}
|
|
969
|
+
|
|
764
970
|
// Inject monograph hint for complex tasks.
|
|
765
971
|
// Source of truth is .monomind/monograph.db (SQLite). Legacy stats.json
|
|
766
972
|
// is no longer written by the build, so it is checked only as a fallback.
|
|
@@ -888,7 +1094,8 @@ const handlers = {
|
|
|
888
1094
|
|
|
889
1095
|
'pre-bash': () => {
|
|
890
1096
|
var _rawCmd = hookInput.command || prompt;
|
|
891
|
-
var
|
|
1097
|
+
var rawCmdStr = (typeof _rawCmd === 'string' ? _rawCmd : String(_rawCmd || ''));
|
|
1098
|
+
var cmd = rawCmdStr.toLowerCase();
|
|
892
1099
|
var dangerous = ['rm -rf /', 'rm -rf/', 'format c:', 'del /s /q c:\\', ':(){:|:&};:'];
|
|
893
1100
|
for (var i = 0; i < dangerous.length; i++) {
|
|
894
1101
|
if (cmd.includes(dangerous[i])) {
|
|
@@ -896,6 +1103,43 @@ const handlers = {
|
|
|
896
1103
|
process.exit(1);
|
|
897
1104
|
}
|
|
898
1105
|
}
|
|
1106
|
+
|
|
1107
|
+
// Intercept Bash-side grep/rg/find/ag — close the loophole where LLM
|
|
1108
|
+
// bypasses the Grep tool by shelling out. Same monograph hint shape as pre-search.
|
|
1109
|
+
try {
|
|
1110
|
+
// Match: grep <flags> <pattern>, rg <flags> <pattern>, ag <pattern>, find . -name <pattern>
|
|
1111
|
+
var grepMatch = rawCmdStr.match(/\b(?:grep|rg|ag)\b(?:\s+-[a-zA-Z]+)*\s+(?:"([^"]+)"|'([^']+)'|([^\s|;&<>]+))/);
|
|
1112
|
+
var findMatch = rawCmdStr.match(/\bfind\b.*?-name\s+(?:"([^"]+)"|'([^']+)'|([^\s|;&<>]+))/);
|
|
1113
|
+
var pattern = '';
|
|
1114
|
+
if (grepMatch) {
|
|
1115
|
+
pattern = grepMatch[1] || grepMatch[2] || grepMatch[3] || '';
|
|
1116
|
+
_recordGraphTelemetry('bash_grep_call');
|
|
1117
|
+
} else if (findMatch) {
|
|
1118
|
+
pattern = findMatch[1] || findMatch[2] || findMatch[3] || '';
|
|
1119
|
+
_recordGraphTelemetry('bash_find_call');
|
|
1120
|
+
}
|
|
1121
|
+
if (pattern && pattern.length >= 3) {
|
|
1122
|
+
var sigB = 'Bash-grep:' + pattern.slice(0, 60);
|
|
1123
|
+
var countB = _recordToolCall(sigB);
|
|
1124
|
+
if (countB >= 3) {
|
|
1125
|
+
console.log('[LOOP_DRIFT] You\'ve grepped "' + pattern.slice(0, 40) + '" ' + countB + 'x — switch to monograph_query.');
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
if (pattern && pattern.length >= 3) {
|
|
1129
|
+
var clean = pattern.replace(/[\\^$.*+?()\[\]{}|]/g, ' ').trim();
|
|
1130
|
+
if (clean.length >= 3) {
|
|
1131
|
+
var hits = getMonographSuggestions(clean, 5);
|
|
1132
|
+
if (hits.length > 0) {
|
|
1133
|
+
console.log('[MONOGRAPH_HIT] Graph has ' + hits.length + ' file(s) for "' + clean.slice(0, 40) + '" — consider monograph_query instead of shell grep:');
|
|
1134
|
+
for (var j = 0; j < hits.length; j++) {
|
|
1135
|
+
var h = hits[j];
|
|
1136
|
+
console.log(' · ' + h.name + ' [' + h.label + '] — ' + (h.file || ''));
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
} catch (e) { /* non-fatal */ }
|
|
1142
|
+
|
|
899
1143
|
console.log('[OK] Command validated');
|
|
900
1144
|
},
|
|
901
1145
|
|
|
@@ -909,6 +1153,13 @@ const handlers = {
|
|
|
909
1153
|
if (typeof pattern !== 'string' || pattern.length < 3) return;
|
|
910
1154
|
var clean = pattern.replace(/[\\^$.*+?()\[\]{}|]/g, ' ').trim();
|
|
911
1155
|
if (clean.length < 3) return;
|
|
1156
|
+
|
|
1157
|
+
// Loop drift detection
|
|
1158
|
+
var sig = (toolName || 'Search') + ':' + clean.slice(0, 60);
|
|
1159
|
+
var count = _recordToolCall(sig);
|
|
1160
|
+
if (count >= 3) {
|
|
1161
|
+
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.');
|
|
1162
|
+
}
|
|
912
1163
|
var suggestions = getMonographSuggestions(clean, 5);
|
|
913
1164
|
if (suggestions.length === 0) return;
|
|
914
1165
|
console.log('[MONOGRAPH_HIT] Graph already knows ' + suggestions.length + ' file(s) matching "' + clean.slice(0, 40) + '":');
|
|
@@ -956,6 +1207,24 @@ const handlers = {
|
|
|
956
1207
|
intelligence.recordEdit(file);
|
|
957
1208
|
} catch (e) { /* non-fatal */ }
|
|
958
1209
|
}
|
|
1210
|
+
// Increment write counter and rebuild monograph when threshold hit.
|
|
1211
|
+
_maybeRebuildMonograph();
|
|
1212
|
+
|
|
1213
|
+
// Test feedback (detection-only): when editing a source file, list tests
|
|
1214
|
+
// that import it so the LLM/user knows what to verify next.
|
|
1215
|
+
try {
|
|
1216
|
+
var editedFile = hookInput.file_path || toolInput.file_path
|
|
1217
|
+
|| process.env.TOOL_INPUT_file_path || args[0] || '';
|
|
1218
|
+
if (editedFile && !editedFile.match(/\.(test|spec)\./) && !editedFile.includes('__tests__')) {
|
|
1219
|
+
var affectedTests = _findAffectedTests(editedFile);
|
|
1220
|
+
if (affectedTests.length > 0) {
|
|
1221
|
+
console.log('[AFFECTED_TESTS] ' + affectedTests.length + ' test(s) cover this file:');
|
|
1222
|
+
for (var ti = 0; ti < Math.min(5, affectedTests.length); ti++) {
|
|
1223
|
+
console.log(' · ' + affectedTests[ti]);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
} catch (e) {}
|
|
959
1228
|
// ── Security-Sensitive File Auto-Alert ────────────────────────────────
|
|
960
1229
|
// When editing auth, security, crypto, or env-related files, flag it
|
|
961
1230
|
try {
|
|
@@ -1886,6 +2155,47 @@ const handlers = {
|
|
|
1886
2155
|
'utf-8'
|
|
1887
2156
|
);
|
|
1888
2157
|
} catch (e) { /* non-fatal — never block a subagent from starting */ }
|
|
2158
|
+
|
|
2159
|
+
// Subagent context inheritance — inject graph god nodes + parent's last
|
|
2160
|
+
// pre-resolved suggestions so the spawned agent inherits spatial map
|
|
2161
|
+
// instead of starting blind.
|
|
2162
|
+
try {
|
|
2163
|
+
var subDb = _openMonographDb();
|
|
2164
|
+
if (subDb) {
|
|
2165
|
+
try {
|
|
2166
|
+
var godRows = subDb.prepare(
|
|
2167
|
+
"SELECT n.name, n.label, n.file_path AS file, " +
|
|
2168
|
+
"(SELECT COUNT(*) FROM edges WHERE source_id=n.id OR target_id=n.id) AS deg " +
|
|
2169
|
+
"FROM nodes n " +
|
|
2170
|
+
"WHERE n.label NOT IN ('Concept') AND n.file_path IS NOT NULL AND n.file_path != '' " +
|
|
2171
|
+
"ORDER BY deg DESC LIMIT 5"
|
|
2172
|
+
).all();
|
|
2173
|
+
if (godRows.length > 0) {
|
|
2174
|
+
console.log('[MONOGRAPH_SUBAGENT_CTX] Graph map inherited from parent:');
|
|
2175
|
+
for (var gi = 0; gi < godRows.length; gi++) {
|
|
2176
|
+
var gr = godRows[gi];
|
|
2177
|
+
console.log(' · ' + gr.name + ' [' + gr.label + '] — ' + (gr.file || '') + ' (deg ' + gr.deg + ')');
|
|
2178
|
+
}
|
|
2179
|
+
// Also forward parent's last routing suggestion text if any
|
|
2180
|
+
try {
|
|
2181
|
+
var subAgentDesc = hookInput.description || hookInput.prompt_description || '';
|
|
2182
|
+
if (subAgentDesc && subAgentDesc.length > 8) {
|
|
2183
|
+
var subHints = getMonographSuggestions(subAgentDesc, 3);
|
|
2184
|
+
if (subHints.length > 0) {
|
|
2185
|
+
console.log(' Top files for this subagent task:');
|
|
2186
|
+
for (var si2 = 0; si2 < subHints.length; si2++) {
|
|
2187
|
+
var sh = subHints[si2];
|
|
2188
|
+
console.log(' · ' + sh.name + ' [' + sh.label + '] — ' + (sh.file || ''));
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
} catch (_) {}
|
|
2193
|
+
console.log(' Use mcp__monomind__monograph_suggest / monograph_query in this subagent before grepping.');
|
|
2194
|
+
}
|
|
2195
|
+
} finally { try { subDb.close(); } catch (_) {} }
|
|
2196
|
+
}
|
|
2197
|
+
} catch (e) { /* non-fatal */ }
|
|
2198
|
+
|
|
1889
2199
|
console.log('[OK] Agent registered');
|
|
1890
2200
|
},
|
|
1891
2201
|
|
|
@@ -1903,15 +2213,18 @@ const handlers = {
|
|
|
1903
2213
|
};
|
|
1904
2214
|
|
|
1905
2215
|
if (command && handlers[command]) {
|
|
2216
|
+
var _hookStart = Date.now();
|
|
1906
2217
|
try {
|
|
1907
2218
|
await Promise.resolve(handlers[command]());
|
|
1908
2219
|
} catch (e) {
|
|
1909
2220
|
console.log('[WARN] Hook ' + command + ' encountered an error: ' + e.message);
|
|
2221
|
+
} finally {
|
|
2222
|
+
try { _recordHookLatency(command, Date.now() - _hookStart); } catch (_) {}
|
|
1910
2223
|
}
|
|
1911
2224
|
} else if (command) {
|
|
1912
2225
|
console.log('[OK] Hook: ' + command);
|
|
1913
2226
|
} else {
|
|
1914
|
-
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>');
|
|
2227
|
+
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>');
|
|
1915
2228
|
}
|
|
1916
2229
|
}
|
|
1917
2230
|
|
|
@@ -696,20 +696,46 @@ function getGraphifyStats() {
|
|
|
696
696
|
return { nodes: 0, edges: 0, exists: false };
|
|
697
697
|
}
|
|
698
698
|
|
|
699
|
-
//
|
|
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
|
+
|
|
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');
|
|
702
726
|
try {
|
|
703
727
|
if (!fs.existsSync(usagePath)) return null;
|
|
704
728
|
const d = JSON.parse(fs.readFileSync(usagePath, 'utf-8'));
|
|
705
729
|
const monograph = d.monograph_call || 0;
|
|
706
|
-
const search = (d.grep_call || 0) + (d.glob_call || 0)
|
|
730
|
+
const search = (d.grep_call || 0) + (d.glob_call || 0)
|
|
731
|
+
+ (d.bash_grep_call || 0) + (d.bash_find_call || 0);
|
|
707
732
|
const total = monograph + search;
|
|
708
733
|
if (total === 0) return null;
|
|
709
734
|
return {
|
|
710
735
|
monograph,
|
|
711
736
|
search,
|
|
712
737
|
pct: Math.round((monograph / total) * 100),
|
|
738
|
+
dollarsSaved: d.dollars_saved || 0,
|
|
713
739
|
};
|
|
714
740
|
} catch { return null; }
|
|
715
741
|
}
|
|
@@ -1257,15 +1283,27 @@ function generateDashboard() {
|
|
|
1257
1283
|
loopStr = `${x.slate}🔄 no active loops${x.reset}`;
|
|
1258
1284
|
}
|
|
1259
1285
|
|
|
1260
|
-
// Graph usage ratio — show only when there's data
|
|
1286
|
+
// Graph usage ratio + $ saved — show only when there's data
|
|
1261
1287
|
const usage = getGraphUsage();
|
|
1262
1288
|
let usageStr = '';
|
|
1263
1289
|
if (usage) {
|
|
1264
1290
|
const col = usage.pct >= 40 ? x.green : usage.pct >= 15 ? x.gold : x.coral;
|
|
1265
|
-
|
|
1291
|
+
const saved = usage.dollarsSaved > 0
|
|
1292
|
+
? ` ${x.green}💰 +$${usage.dollarsSaved.toFixed(2)}${x.reset}`
|
|
1293
|
+
: '';
|
|
1294
|
+
usageStr = ` ${DIV} ${col}📊 graph ${usage.pct}%${x.reset}${x.slate} · grep ${100 - usage.pct}%${x.reset}${saved}`;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
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}`;
|
|
1266
1304
|
}
|
|
1267
1305
|
|
|
1268
|
-
lines.push(`${x.purple}🤖 AGENT${x.reset} ${agentStr} ${DIV} ${loopStr}${usageStr}`);
|
|
1306
|
+
lines.push(`${x.purple}🤖 AGENT${x.reset} ${agentStr} ${DIV} ${loopStr}${usageStr}${latStr}`);
|
|
1269
1307
|
lines.push(SEP);
|
|
1270
1308
|
|
|
1271
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,
|
|
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,17 +828,39 @@ function getTriggerStats() {
|
|
|
828
828
|
} catch { return { triggers: 0, agents: 0 }; }
|
|
829
829
|
}
|
|
830
830
|
|
|
831
|
-
//
|
|
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
|
+
|
|
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');
|
|
834
855
|
try {
|
|
835
856
|
if (!fs.existsSync(usagePath)) return null;
|
|
836
857
|
const d = JSON.parse(fs.readFileSync(usagePath, 'utf-8'));
|
|
837
858
|
const monograph = d.monograph_call || 0;
|
|
838
|
-
const search = (d.grep_call || 0) + (d.glob_call || 0)
|
|
859
|
+
const search = (d.grep_call || 0) + (d.glob_call || 0)
|
|
860
|
+
+ (d.bash_grep_call || 0) + (d.bash_find_call || 0);
|
|
839
861
|
const total = monograph + search;
|
|
840
862
|
if (total === 0) return null;
|
|
841
|
-
return { monograph: monograph, search: search, pct: Math.round((monograph / total) * 100) };
|
|
863
|
+
return { monograph: monograph, search: search, pct: Math.round((monograph / total) * 100), dollarsSaved: d.dollars_saved || 0 };
|
|
842
864
|
} catch { return null; }
|
|
843
865
|
}
|
|
844
866
|
|
|
@@ -1084,15 +1106,27 @@ function generateDashboard() {
|
|
|
1084
1106
|
loopStr = \`\${x.slate}🔄 no active loops\${x.reset}\`;
|
|
1085
1107
|
}
|
|
1086
1108
|
|
|
1087
|
-
// Graph usage ratio — show only when there's data
|
|
1109
|
+
// Graph usage ratio + $ saved — show only when there's data
|
|
1088
1110
|
const usage = getGraphUsage();
|
|
1089
1111
|
let usageStr = '';
|
|
1090
1112
|
if (usage) {
|
|
1091
1113
|
const col = usage.pct >= 40 ? x.green : usage.pct >= 15 ? x.gold : x.coral;
|
|
1092
|
-
|
|
1114
|
+
const saved = usage.dollarsSaved > 0
|
|
1115
|
+
? \` \${x.green}💰 +$\${usage.dollarsSaved.toFixed(2)}\${x.reset}\`
|
|
1116
|
+
: '';
|
|
1117
|
+
usageStr = \` \${DIV} \${col}📊 graph \${usage.pct}%\${x.reset}\${x.slate} · grep \${100 - usage.pct}%\${x.reset}\${saved}\`;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
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}\`;
|
|
1093
1127
|
}
|
|
1094
1128
|
|
|
1095
|
-
lines.push(\`\${x.purple}🤖 AGENT\${x.reset} \${agentStr} \${DIV} \${loopStr}\${usageStr}\`);
|
|
1129
|
+
lines.push(\`\${x.purple}🤖 AGENT\${x.reset} \${agentStr} \${DIV} \${loopStr}\${usageStr}\${latStr}\`);
|
|
1096
1130
|
lines.push(SEP);
|
|
1097
1131
|
|
|
1098
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
|
|
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"}
|