@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.
- package/.claude/commands/monomind/adr.md +11 -0
- package/.claude/helpers/hook-handler.cjs +254 -4
- package/.claude/helpers/statusline.cjs +34 -1
- package/dist/src/init/statusline-generator.d.ts.map +1 -1
- package/dist/src/init/statusline-generator.js +31 -1
- package/dist/src/init/statusline-generator.js.map +1 -1
- package/dist/src/ui/server.mjs +64 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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"}
|
package/dist/src/ui/server.mjs
CHANGED
|
@@ -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') {
|