@monoes/monomindcli 1.10.29 → 1.10.30

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.
Files changed (80) hide show
  1. package/.claude/helpers/auto-memory-hook.mjs +39 -4
  2. package/.claude/helpers/handlers/edit-handler.cjs +145 -0
  3. package/.claude/helpers/handlers/route-handler.cjs +393 -0
  4. package/.claude/helpers/handlers/session-handler.cjs +167 -0
  5. package/.claude/helpers/handlers/session-restore-handler.cjs +343 -0
  6. package/.claude/helpers/handlers/task-handler.cjs +329 -0
  7. package/.claude/helpers/hook-handler.cjs +114 -2273
  8. package/.claude/helpers/intelligence.cjs +21 -2
  9. package/.claude/helpers/learning-service.mjs +166 -8
  10. package/.claude/helpers/memory-palace.cjs +72 -12
  11. package/.claude/helpers/router.cjs +79 -5
  12. package/.claude/helpers/statusline.cjs +193 -399
  13. package/.claude/helpers/utils/micro-agents.cjs +338 -0
  14. package/.claude/helpers/utils/monograph.cjs +349 -0
  15. package/.claude/helpers/utils/telemetry.cjs +144 -0
  16. package/.claude/skills/agent-browser-testing/SKILL.md +3 -2
  17. package/.claude/skills/monomind/browse-agentcore.md +116 -0
  18. package/.claude/skills/monomind/browse-electron.md +189 -0
  19. package/.claude/skills/monomind/browse-qa.md +229 -0
  20. package/.claude/skills/monomind/browse-references/authentication.md +162 -0
  21. package/.claude/skills/monomind/browse-references/trust-boundaries.md +41 -0
  22. package/.claude/skills/monomind/browse-references/video-recording.md +84 -0
  23. package/.claude/skills/monomind/browse-slack.md +189 -0
  24. package/.claude/skills/monomind/browse-vercel.md +240 -0
  25. package/.claude/skills/monomind/browse.md +724 -0
  26. package/dist/src/browser/actions.d.ts +13 -0
  27. package/dist/src/browser/actions.d.ts.map +1 -0
  28. package/dist/src/browser/actions.js +201 -0
  29. package/dist/src/browser/actions.js.map +1 -0
  30. package/dist/src/browser/browser.d.ts +14 -0
  31. package/dist/src/browser/browser.d.ts.map +1 -0
  32. package/dist/src/browser/browser.js +198 -0
  33. package/dist/src/browser/browser.js.map +1 -0
  34. package/dist/src/browser/cdp.d.ts +17 -0
  35. package/dist/src/browser/cdp.d.ts.map +1 -0
  36. package/dist/src/browser/cdp.js +106 -0
  37. package/dist/src/browser/cdp.js.map +1 -0
  38. package/dist/src/browser/index.d.ts +11 -0
  39. package/dist/src/browser/index.d.ts.map +1 -0
  40. package/dist/src/browser/index.js +11 -0
  41. package/dist/src/browser/index.js.map +1 -0
  42. package/dist/src/browser/network.d.ts +11 -0
  43. package/dist/src/browser/network.d.ts.map +1 -0
  44. package/dist/src/browser/network.js +81 -0
  45. package/dist/src/browser/network.js.map +1 -0
  46. package/dist/src/browser/screenshot.d.ts +15 -0
  47. package/dist/src/browser/screenshot.d.ts.map +1 -0
  48. package/dist/src/browser/screenshot.js +36 -0
  49. package/dist/src/browser/screenshot.js.map +1 -0
  50. package/dist/src/browser/session.d.ts +8 -0
  51. package/dist/src/browser/session.d.ts.map +1 -0
  52. package/dist/src/browser/session.js +50 -0
  53. package/dist/src/browser/session.js.map +1 -0
  54. package/dist/src/browser/snapshot.d.ts +12 -0
  55. package/dist/src/browser/snapshot.d.ts.map +1 -0
  56. package/dist/src/browser/snapshot.js +147 -0
  57. package/dist/src/browser/snapshot.js.map +1 -0
  58. package/dist/src/browser/tabs.d.ts +8 -0
  59. package/dist/src/browser/tabs.d.ts.map +1 -0
  60. package/dist/src/browser/tabs.js +25 -0
  61. package/dist/src/browser/tabs.js.map +1 -0
  62. package/dist/src/browser/types.d.ts +109 -0
  63. package/dist/src/browser/types.d.ts.map +1 -0
  64. package/dist/src/browser/types.js +16 -0
  65. package/dist/src/browser/types.js.map +1 -0
  66. package/dist/src/browser/wait.d.ts +4 -0
  67. package/dist/src/browser/wait.d.ts.map +1 -0
  68. package/dist/src/browser/wait.js +122 -0
  69. package/dist/src/browser/wait.js.map +1 -0
  70. package/dist/src/commands/browse.d.ts +8 -0
  71. package/dist/src/commands/browse.d.ts.map +1 -0
  72. package/dist/src/commands/browse.js +573 -0
  73. package/dist/src/commands/browse.js.map +1 -0
  74. package/dist/src/commands/index.d.ts.map +1 -1
  75. package/dist/src/commands/index.js +2 -0
  76. package/dist/src/commands/index.js.map +1 -1
  77. package/dist/src/ui/dashboard-v2.html +1692 -0
  78. package/dist/src/ui/server.mjs +15 -1
  79. package/dist/tsconfig.tsbuildinfo +1 -1
  80. package/package.json +2 -1
@@ -10,474 +10,61 @@ const fs = require('fs');
10
10
  const helpersDir = __dirname;
11
11
  const CWD = process.env.CLAUDE_PROJECT_DIR || process.cwd();
12
12
 
13
- // Resolve @monoes/monograph — it lives in pnpm's virtual store, not a named symlink.
14
- // Try common locations; fall back gracefully so all hooks remain non-fatal.
15
- function _requireMonograph() {
16
- var candidates = [
17
- path.join(CWD, 'node_modules/.pnpm/node_modules/@monoes/monograph'),
18
- path.join(CWD, 'packages/node_modules/.pnpm/node_modules/@monoes/monograph'),
19
- path.join(CWD, 'node_modules/@monoes/monograph'),
20
- ];
21
- for (var i = 0; i < candidates.length; i++) {
22
- try { if (fs.existsSync(candidates[i])) return require(candidates[i]); } catch(e) {}
23
- }
24
- try { return require('@monoes/monograph'); } catch(e) {}
25
- return null;
26
- }
27
-
28
- // ── Monograph LLM-context helpers ──────────────────────────────────────────────
29
- // Used by route (pre-resolve), pre-search (Grep/Glob redirect), and post-read
30
- // (neighbor footer). All calls are best-effort; failures are silent.
31
-
32
- // Memoized at module scope — opening a multi-GB monograph.db can take 7-10s,
33
- // and we call this 3+ times per route hook. Cache for the lifetime of this
34
- // hook process. Callers should NOT close the returned handle.
35
- var _cachedMonographDb = undefined;
36
- function _openMonographDb() {
37
- if (_cachedMonographDb !== undefined) return _cachedMonographDb;
38
- try {
39
- var dbPath = path.join(CWD, '.monomind', 'monograph.db');
40
- if (!fs.existsSync(dbPath)) { _cachedMonographDb = null; return null; }
41
- var mod = _requireMonograph();
42
- if (!mod || !mod.openDb) { _cachedMonographDb = null; return null; }
43
- _cachedMonographDb = mod.openDb(dbPath);
44
- return _cachedMonographDb;
45
- } catch (e) { _cachedMonographDb = null; return null; }
46
- }
47
-
48
- function getMonographSuggestions(taskText, limit) {
49
- if (!taskText || typeof taskText !== "string") return [];
50
- var db = _openMonographDb();
51
- if (!db) return [];
52
- try {
53
- var words = String(taskText).toLowerCase().match(/[a-z][a-z0-9_-]{3,}/g) || [];
54
- 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 };
55
- var uniq = {};
56
- for (var i = 0; i < words.length; i++) if (!stop[words[i]]) uniq[words[i]] = 1;
57
- var keys = Object.keys(uniq).slice(0, 8);
58
- // Smart filter: free-form prompts need ≥2 content words to avoid noise.
59
- // Single-word inputs are allowed only when they look like a symbol/search
60
- // (entire string is ≤30 chars and contains letters+separators only).
61
- var isSymbolLookup = taskText.length <= 30 && /^[a-zA-Z0-9_\-./:]+$/.test(taskText.trim());
62
- if (keys.length === 0) return [];
63
- if (keys.length < 2 && !isSymbolLookup) return [];
64
-
65
- var ftsQuery = keys.map(function(k){ return '"' + k.replace(/"/g, "") + '"'; }).join(" OR ");
66
- var lim = Math.max(1, limit || 5);
67
- var rows = [];
68
- try {
69
- // BM25 ranks better than degree for keyword relevance; tie-break by deg.
70
- // File/Function/Class outrank Section so navigable nodes win.
71
- // Filter out anonymous lambdas, arrow expressions, and other unnamed
72
- // garbage that the AST extraction picks up but isn't navigable.
73
- rows = db.prepare(
74
- "SELECT n.id, n.name, n.label, n.file_path AS file, " +
75
- "bm25(nodes_fts) AS bm25_score, " +
76
- "(SELECT COUNT(*) FROM edges WHERE source_id=n.id OR target_id=n.id) AS deg, " +
77
- "CASE n.label WHEN 'File' THEN 3 WHEN 'Function' THEN 3 WHEN 'Class' THEN 3 " +
78
- " WHEN 'Method' THEN 2 WHEN 'Interface' THEN 2 ELSE 1 END AS label_rank " +
79
- "FROM nodes_fts f JOIN nodes n ON f.rowid = n.rowid " +
80
- "WHERE nodes_fts MATCH ? AND n.file_path IS NOT NULL AND n.file_path != '' " +
81
- "AND n.label NOT IN ('Concept') " +
82
- "AND n.name NOT LIKE '(%' AND n.name NOT LIKE '%=>%' AND n.name != 'function' " +
83
- "AND length(n.name) >= 3 " +
84
- "ORDER BY label_rank DESC, bm25_score ASC, deg DESC LIMIT ?"
85
- ).all(ftsQuery, lim);
86
- } catch (e) {
87
- var likeFrag = keys.map(function(){ return "lower(n.name) LIKE ?"; }).join(" OR ");
88
- var likeArgs = keys.map(function(k){ return "%" + k + "%"; });
89
- var stmt = db.prepare(
90
- "SELECT n.id, n.name, n.label, n.file_path AS file, " +
91
- "(SELECT COUNT(*) FROM edges WHERE source_id=n.id OR target_id=n.id) AS deg " +
92
- "FROM nodes n WHERE (" + likeFrag + ") AND n.file_path IS NOT NULL AND n.file_path != '' " +
93
- "AND n.label NOT IN ('Concept') " +
94
- "ORDER BY deg DESC LIMIT ?"
95
- );
96
- rows = stmt.all.apply(stmt, likeArgs.concat([lim]));
97
- }
98
- return rows || [];
99
- } catch (e) { return []; }
100
- finally { /* db is shared/cached; do not close */ }
101
- }
102
-
103
- function getMonographNeighbors(filePath) {
104
- if (!filePath) return null;
105
- var db = _openMonographDb();
106
- if (!db) return null;
107
- try {
108
- var rel = filePath;
109
- if (filePath.indexOf(CWD) === 0) rel = filePath.slice(CWD.length + 1);
110
- var node = db.prepare(
111
- "SELECT id, name FROM nodes WHERE label='File' AND (file_path=? OR file_path=? OR name=? OR name=?) LIMIT 1"
112
- ).get(filePath, rel, filePath, rel);
113
- if (!node) return null;
114
-
115
- var imports = db.prepare(
116
- "SELECT DISTINCT n.name FROM edges e JOIN nodes n ON e.target_id = n.id " +
117
- "WHERE e.source_id=? AND e.relation IN ('IMPORTS','CALLS','DEPENDS_ON','CONTAINS','DEFINES') " +
118
- "AND n.file_path IS NOT NULL AND n.file_path != '' LIMIT 6"
119
- ).all(node.id).map(function(r){ return r.name; });
120
- var importedBy = db.prepare(
121
- "SELECT DISTINCT n.name FROM edges e JOIN nodes n ON e.source_id = n.id " +
122
- "WHERE e.target_id=? AND e.relation IN ('IMPORTS','CALLS','DEPENDS_ON','CONTAINS','DEFINES') " +
123
- "AND n.file_path IS NOT NULL AND n.file_path != '' LIMIT 6"
124
- ).all(node.id).map(function(r){ return r.name; });
125
-
126
- return { imports: imports, importedBy: importedBy };
127
- } catch (e) { return null; }
128
- finally { /* db is shared/cached; do not close */ }
129
- }
130
-
131
- // Rough per-event token + USD cost estimates. Tuned to Sonnet input pricing
132
- // ($3/M tokens) — adjust if needed. Used by the statusline to surface savings.
133
- var _TOKEN_PER_EVENT = {
134
- monograph_call: 300, // typical monograph_query result size
135
- grep_call: 2000, // typical Grep tool output across many files
136
- glob_call: 800,
137
- bash_grep_call: 2000,
138
- bash_find_call: 800,
139
- };
140
- var _DOLLAR_PER_1M_TOKENS = 3.0;
141
-
142
- function _recordGraphTelemetry(event) {
143
- try {
144
- var metricsDir = path.join(CWD, ".monomind", "metrics");
145
- var f = path.join(metricsDir, "graph-usage.json");
146
- fs.mkdirSync(metricsDir, { recursive: true });
147
- var d = {};
148
- try { d = JSON.parse(fs.readFileSync(f, "utf-8")); } catch (e) {}
149
- if (typeof d !== "object" || d === null) d = {};
150
- d[event] = (d[event] || 0) + 1;
151
-
152
- // Token-saved estimator: each monograph_call avoids a grep equivalent
153
- // (~2000 tokens) at a cost of ~300 tokens — net ~1700 saved.
154
- if (event === 'monograph_call') {
155
- var saved = (_TOKEN_PER_EVENT.grep_call - _TOKEN_PER_EVENT.monograph_call);
156
- d.tokens_saved = (d.tokens_saved || 0) + saved;
157
- d.dollars_saved = ((d.tokens_saved / 1000000) * _DOLLAR_PER_1M_TOKENS);
158
- }
159
- // Each unprompted grep/bash_grep "wastes" the same amount vs the graph alternative.
160
- if (event === 'grep_call' || event === 'bash_grep_call') {
161
- var wasted = (_TOKEN_PER_EVENT.grep_call - _TOKEN_PER_EVENT.monograph_call);
162
- d.tokens_wasted = (d.tokens_wasted || 0) + wasted;
163
- }
164
-
165
- d.lastUpdated = Date.now();
166
- fs.writeFileSync(f, JSON.stringify(d));
167
- } catch (e) { /* non-fatal */ }
168
- }
169
-
170
- // Re-inject graph context after compaction so the LLM doesn't lose its spatial map.
171
- // Prefers recently-edited files (session context) over pure degree centrality so
172
- // the injected anchors match what the LLM was actually working on.
173
- function _injectCompactGraphMap() {
174
- try {
175
- var db = _openMonographDb();
176
- if (!db) return;
177
- try {
178
- var nodeC = db.prepare("SELECT COUNT(*) AS c FROM nodes").get().c;
179
- var anchors = [];
180
- var seenPaths = {};
181
-
182
- // 1. Prefer recently-edited files (up to 5) — these are what matters NOW.
183
- var recentEdits = _getRecentEdits();
184
- for (var ri = 0; ri < Math.min(recentEdits.length, 5); ri++) {
185
- var rfile = recentEdits[ri].file;
186
- // Normalise to relative path for DB lookup
187
- var rrel = (rfile.indexOf(CWD) === 0) ? rfile.slice(CWD.length + 1) : rfile;
188
- try {
189
- var rnode = db.prepare(
190
- "SELECT n.name, n.label, n.file_path, " +
191
- "(SELECT COUNT(*) FROM edges WHERE source_id=n.id OR target_id=n.id) AS deg " +
192
- "FROM nodes n WHERE n.label='File' AND (n.file_path=? OR n.file_path=?) LIMIT 1"
193
- ).get(rfile, rrel);
194
- if (rnode && !seenPaths[rnode.file_path]) {
195
- seenPaths[rnode.file_path] = 1;
196
- anchors.push({ name: rnode.name, label: rnode.label, file_path: rnode.file_path, deg: rnode.deg, tag: '✎' });
197
- }
198
- } catch (e) { /* ignore — file may not be in graph yet */ }
199
- }
200
-
201
- // 2. Fill remaining slots (up to 8 total) with god nodes (high centrality).
202
- // Exclude node_modules paths (external typings like `Path [Interface]` skew rankings).
203
- if (anchors.length < 8) {
204
- var gods = db.prepare(
205
- "SELECT n.name, n.label, n.file_path, " +
206
- "(SELECT COUNT(*) FROM edges WHERE source_id=n.id OR target_id=n.id) AS deg " +
207
- "FROM nodes n " +
208
- "WHERE n.label NOT IN ('Concept') AND n.file_path IS NOT NULL AND n.file_path != '' " +
209
- "AND n.file_path NOT LIKE '%/node_modules/%' AND n.file_path NOT LIKE '%node_modules%' " +
210
- "AND n.name NOT LIKE '(%' AND n.name NOT LIKE '%=>%' AND length(n.name) >= 3 " +
211
- "ORDER BY deg DESC LIMIT 15"
212
- ).all();
213
- for (var gi = 0; gi < gods.length && anchors.length < 8; gi++) {
214
- if (!seenPaths[gods[gi].file_path]) {
215
- seenPaths[gods[gi].file_path] = 1;
216
- anchors.push({ name: gods[gi].name, label: gods[gi].label, file_path: gods[gi].file_path, deg: gods[gi].deg, tag: '' });
217
- }
218
- }
219
- }
220
-
221
- if (anchors.length > 0) {
222
- console.log('[COMPACT_GRAPH] ' + nodeC + ' nodes. Session context (✎ = recently edited):');
223
- for (var ci = 0; ci < anchors.length; ci++) {
224
- var g = anchors[ci];
225
- console.log(' ' + (g.tag || ' ') + ' ' + g.name + ' [' + g.label + '] — ' + g.file_path + ' (deg ' + g.deg + ')');
226
- }
227
- console.log(' Use mcp__monomind__monograph_suggest first when navigating.');
228
- }
229
- } finally { /* db is shared/cached; do not close */ }
230
- } catch (e) {}
231
- }
232
-
233
- // ── Recent edit history ────────────────────────────────────────────────────────
234
- // Track last N edited file paths so compact injection and pre-resolve can surface
235
- // the files the LLM was actively working on instead of pure centrality anchors.
236
- function _recordRecentEdit(filePath) {
237
- if (!filePath) return;
238
- try {
239
- var f = path.join(CWD, '.monomind', 'metrics', 'recent-edits.json');
240
- fs.mkdirSync(path.dirname(f), { recursive: true });
241
- var d = { edits: [] };
242
- try { d = JSON.parse(fs.readFileSync(f, 'utf-8')); } catch (_) {}
243
- if (!Array.isArray(d.edits)) d.edits = [];
244
- // Remove stale entry for same file, then prepend fresh one
245
- d.edits = d.edits.filter(function(e) { return e.file !== filePath; });
246
- d.edits.unshift({ file: filePath, editedAt: Date.now() });
247
- if (d.edits.length > 10) d.edits = d.edits.slice(0, 10);
248
- fs.writeFileSync(f, JSON.stringify(d));
249
- } catch (e) { /* non-fatal */ }
250
- }
251
-
252
- function _getRecentEdits() {
253
- try {
254
- var f = path.join(CWD, '.monomind', 'metrics', 'recent-edits.json');
255
- if (!fs.existsSync(f)) return [];
256
- var d = JSON.parse(fs.readFileSync(f, 'utf-8'));
257
- if (!Array.isArray(d.edits)) return [];
258
- // Only return edits from the last 2 hours (session-scoped)
259
- var cutoff = Date.now() - 2 * 60 * 60 * 1000;
260
- return d.edits.filter(function(e) { return e.editedAt > cutoff; });
261
- } catch (e) { return []; }
262
- }
263
-
264
- // ── Loop drift detection ───────────────────────────────────────────────────────
265
- // Record tool call signatures per session, warn when the same call recurs ≥3×.
266
- function _recordToolCall(signature) {
267
- try {
268
- var f = path.join(CWD, '.monomind', 'metrics', 'tool-calls.json');
269
- fs.mkdirSync(path.dirname(f), { recursive: true });
270
- var d = {};
271
- try { d = JSON.parse(fs.readFileSync(f, 'utf-8')); } catch (_) {}
272
- if (typeof d !== 'object' || d === null) d = {};
273
- // Roll over every 4 hours (new session)
274
- if (!d.startedAt || (Date.now() - d.startedAt) > 4 * 60 * 60 * 1000) {
275
- d = { startedAt: Date.now(), calls: {} };
276
- }
277
- d.calls[signature] = (d.calls[signature] || 0) + 1;
278
- fs.writeFileSync(f, JSON.stringify(d));
279
- return d.calls[signature];
280
- } catch (e) { return 0; }
281
- }
282
-
283
- // ── Cost budget ────────────────────────────────────────────────────────────────
284
- // Read today's cost from token-summary and compare against budget ceiling.
285
- // If no budget.json exists, auto-tune from 30-day rolling mean (1.5x) so we
286
- // don't shout BUDGET_BREACHED at users whose normal spend is above the default.
287
- function _getBudgetStatus() {
288
- try {
289
- var budgetFile = path.join(CWD, '.monomind', 'budget.json');
290
- var summaryFile = path.join(CWD, '.monomind', 'metrics', 'token-summary.json');
291
- if (!fs.existsSync(summaryFile)) return null;
292
- var summary = JSON.parse(fs.readFileSync(summaryFile, 'utf-8'));
293
- var todayCost = summary.todayCost || (summary.today && summary.today.cost) || 0;
294
- var monthCost = summary.monthCost || (summary.month && summary.month.cost) || 0;
295
-
296
- var dailyLimit, monthlyLimit, autoTuned = false;
297
- if (fs.existsSync(budgetFile)) {
298
- try {
299
- var b = JSON.parse(fs.readFileSync(budgetFile, 'utf-8'));
300
- dailyLimit = b.dailyLimit;
301
- monthlyLimit = b.monthlyLimit;
302
- } catch (_) {}
303
- }
304
-
305
- // Auto-tune: monthCost / daysSoFar = avg daily; 1.5x that = limit.
306
- // Only auto-tune when we actually have 7+ days of data and no manual budget.
307
- if (!dailyLimit || !monthlyLimit) {
308
- var now = new Date();
309
- var daysIntoMonth = now.getUTCDate();
310
- var dailyAvg = daysIntoMonth >= 1 ? monthCost / daysIntoMonth : 0;
311
- if (dailyAvg > 5 && daysIntoMonth >= 7) {
312
- dailyLimit = Math.max(dailyLimit || 0, Math.ceil(dailyAvg * 1.5));
313
- monthlyLimit = Math.max(monthlyLimit || 0, Math.ceil(dailyAvg * 1.5 * 30));
314
- autoTuned = true;
315
- // Persist so future runs are stable and the user can edit.
316
- try {
317
- fs.mkdirSync(path.dirname(budgetFile), { recursive: true });
318
- fs.writeFileSync(budgetFile, JSON.stringify({
319
- dailyLimit: dailyLimit,
320
- monthlyLimit: monthlyLimit,
321
- autoTuned: true,
322
- tunedAt: now.toISOString(),
323
- basis: 'rolling avg $' + dailyAvg.toFixed(2) + '/day × 1.5',
324
- note: 'Edit these values to set a hard ceiling. Delete the file to re-tune.',
325
- }, null, 2));
326
- } catch (_) {}
327
- } else {
328
- // Fall back to sensible defaults when there's not enough history.
329
- dailyLimit = dailyLimit || 50;
330
- monthlyLimit = monthlyLimit || 1500;
331
- }
332
- }
333
-
334
- var dailyPct = Math.round((todayCost / dailyLimit) * 100);
335
- var monthlyPct = Math.round((monthCost / monthlyLimit) * 100);
336
-
337
- // Spike detection: today is >2x the rolling daily avg (suspicious activity)
338
- var rollingDaily = (new Date()).getUTCDate() >= 1 ? monthCost / (new Date()).getUTCDate() : 0;
339
- var spike = rollingDaily > 0 && todayCost > rollingDaily * 2.0 && todayCost > 5;
340
-
341
- return {
342
- todayCost: todayCost, monthCost: monthCost,
343
- dailyLimit: dailyLimit, monthlyLimit: monthlyLimit,
344
- dailyPct: dailyPct, monthlyPct: monthlyPct,
345
- autoTuned: autoTuned,
346
- spike: spike,
347
- // Alert only when either the limit is breached OR there's a real spike
348
- alert: dailyPct >= 80 || monthlyPct >= 80 || spike,
349
- breached: dailyPct >= 100 || monthlyPct >= 100,
350
- };
351
- } catch (e) { return null; }
352
- }
353
-
354
- // ── Test feedback (detection only — do not auto-run) ──────────────────────────
355
- // When LLM edits a source file, find tests that import it via monograph and
356
- // surface the list so the LLM (or user) knows what to verify.
357
- function _findAffectedTests(filePath) {
358
- if (!filePath) return [];
359
- var db = _openMonographDb();
360
- if (!db) return [];
361
- try {
362
- var rel = filePath;
363
- if (filePath.indexOf(CWD) === 0) rel = filePath.slice(CWD.length + 1);
364
- // Find tests that IMPORTS any symbol whose target file_path matches our file.
365
- // The graph stores IMPORTS edges to symbol nodes, not file nodes — so we
366
- // match on the target node's file_path field instead of target_id directly.
367
- var rows = db.prepare(
368
- "SELECT DISTINCT src.file_path FROM edges e " +
369
- "JOIN nodes src ON e.source_id = src.id " +
370
- "JOIN nodes tgt ON e.target_id = tgt.id " +
371
- "WHERE e.relation IN ('IMPORTS','CALLS','DEPENDS_ON') " +
372
- "AND (tgt.file_path = ? OR tgt.file_path = ?) " +
373
- "AND src.file_path IS NOT NULL AND src.file_path != '' " +
374
- "AND (src.file_path LIKE '%test%' OR src.file_path LIKE '%.spec.%' OR src.file_path LIKE '%__tests__%') " +
375
- "AND src.file_path NOT LIKE '%.worktrees%' " +
376
- "LIMIT 5"
377
- ).all(filePath, rel);
378
- return rows.map(function(r) { return r.file_path; });
379
- } catch (e) { return []; }
380
- finally { /* db is shared/cached; do not close */ }
381
- }
382
-
383
- // ── Hook latency tracking ─────────────────────────────────────────────────────
384
- function _recordHookLatency(handlerName, durationMs) {
385
- try {
386
- var f = path.join(CWD, '.monomind', 'metrics', 'hook-latency.json');
387
- fs.mkdirSync(path.dirname(f), { recursive: true });
388
- var d = {};
389
- try { d = JSON.parse(fs.readFileSync(f, 'utf-8')); } catch (_) {}
390
- if (typeof d !== 'object' || d === null) d = {};
391
- var entry = d[handlerName] || { count: 0, total: 0, max: 0 };
392
- entry.count++;
393
- entry.total += durationMs;
394
- entry.max = Math.max(entry.max, durationMs);
395
- entry.mean = Math.round(entry.total / entry.count);
396
- d[handlerName] = entry;
397
- d.lastUpdated = Date.now();
398
- fs.writeFileSync(f, JSON.stringify(d));
399
- } catch (e) {}
400
- }
401
-
402
- // ── Auto-ADR decision detection ───────────────────────────────────────────────
403
- // Record sentence-level decision markers from user prompts to .monomind/decisions.jsonl
404
- function _recordDecisionMarkers(promptText) {
405
- if (!promptText || typeof promptText !== 'string') return;
406
- 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;
407
- var matches = promptText.match(markers);
408
- if (!matches || matches.length === 0) return;
409
- try {
410
- var f = path.join(CWD, '.monomind', 'decisions.jsonl');
411
- var entry = JSON.stringify({
412
- ts: Date.now(),
413
- excerpts: matches.slice(0, 3),
414
- prompt: promptText.slice(0, 400),
415
- });
416
- fs.appendFileSync(f, entry + '\n');
417
- } catch (e) {}
418
- }
419
-
420
- // Auto-rebuild the monograph after N writes — graph staleness during heavy
421
- // editing is the main reason suggestions go cold. Triggered by post-edit.
422
- function _maybeRebuildMonograph() {
423
- try {
424
- var metricsDir = path.join(CWD, ".monomind", "metrics");
425
- fs.mkdirSync(metricsDir, { recursive: true });
426
- var f = path.join(metricsDir, "graph-rebuild.json");
427
- var d = {};
428
- try { d = JSON.parse(fs.readFileSync(f, "utf-8")); } catch (_) {}
429
- if (typeof d !== "object" || d === null) d = {};
430
- d.writesSinceRebuild = (d.writesSinceRebuild || 0) + 1;
431
- d.lastWriteAt = Date.now();
432
- var THRESHOLD = 20;
433
- var MIN_INTERVAL_MS = 5 * 60 * 1000; // never more often than every 5 min
434
- var dueByCount = d.writesSinceRebuild >= THRESHOLD;
435
- var dueByTime = !d.lastRebuildAt || (Date.now() - d.lastRebuildAt) > MIN_INTERVAL_MS;
436
- if (dueByCount && dueByTime) {
437
- // Reset counter immediately so concurrent post-edits don't all fire.
438
- d.writesSinceRebuild = 0;
439
- d.lastRebuildAt = Date.now();
440
- fs.writeFileSync(f, JSON.stringify(d));
441
- // Fire-and-forget background freshen script (same one used by SessionStart).
442
- try {
443
- var freshenScript = path.join(CWD, '.claude', 'helpers', 'graphify-freshen.cjs');
444
- if (fs.existsSync(freshenScript)) {
445
- var spawn = require('child_process').spawn;
446
- var child = spawn(process.execPath, [freshenScript], {
447
- detached: true,
448
- stdio: 'ignore',
449
- cwd: CWD,
450
- });
451
- child.unref();
452
- }
453
- } catch (_) {}
454
- } else {
455
- fs.writeFileSync(f, JSON.stringify(d));
456
- }
457
- } catch (e) { /* non-fatal */ }
458
- }
459
-
460
- function safeRequire(modulePath) {
461
- try {
462
- if (fs.existsSync(modulePath)) {
463
- const origLog = console.log;
464
- const origError = console.error;
465
- console.log = () => {};
466
- console.error = () => {};
13
+ const telemetry = require('./utils/telemetry.cjs');
14
+ const monograph = require('./utils/monograph.cjs');
15
+ const microAgents = require('./utils/micro-agents.cjs');
16
+
17
+ const {
18
+ _recordRecentEdit, _getRecentEdits, _recordToolCall,
19
+ _getBudgetStatus, _recordHookLatency, _recordDecisionMarkers,
20
+ } = telemetry;
21
+
22
+ const {
23
+ _requireMonograph, _openMonographDb,
24
+ getMonographSuggestions, getMonographNeighbors,
25
+ _recordGraphTelemetry, _injectCompactGraphMap,
26
+ _findAffectedTests, _maybeRebuildMonograph,
27
+ } = monograph;
28
+
29
+ const {
30
+ safeRequire,
31
+ _triggerExtractYamlValue, _triggerFinalize, _triggerExtractFromFrontmatter,
32
+ _triggerCollectMdFiles, _triggerBuildIndex, scanMicroAgentTriggers,
33
+ _buildKnowledgeSearchFn, _autoIndexKnowledge,
34
+ } = microAgents;
35
+
36
+ // ── LearningService module-level singleton ─────────────────────────────────────
37
+ // Singleton contract: one LearningService instance is created per hook-handler
38
+ // process. initialize() opens the SQLite DB; consolidate() is called at
39
+ // session-end. Hoisting to module scope ensures the DB is not reopened on every
40
+ // session-end invocation (which would create a fresh in-memory-only instance
41
+ // each time, discarding any state accumulated during the session).
42
+ //
43
+ // We cache the Promise (not the resolved value) so that concurrent callers all
44
+ // await the same initialization. Caching only the resolved value allowed two
45
+ // concurrent callers to both enter the `if (!_learningService)` branch and
46
+ // construct separate LearningService instances, leaving an orphaned DB handle.
47
+ var _learningServicePromise = null;
48
+ async function getLearningService() {
49
+ if (!_learningServicePromise) {
50
+ _learningServicePromise = (async function() {
467
51
  try {
468
- const mod = require(modulePath);
469
- return mod;
470
- } finally {
471
- console.log = origLog;
472
- console.error = origError;
52
+ var lsMod = await import('file://' + path.join(__dirname, 'learning-service.mjs'));
53
+ var LearningService = lsMod.LearningService || (lsMod.default && lsMod.default.LearningService);
54
+ if (!LearningService) return null;
55
+ var svc = new LearningService();
56
+ if (typeof svc.initialize === 'function') await svc.initialize();
57
+ return svc;
58
+ } catch (e) {
59
+ _learningServicePromise = null; // allow retry on error
60
+ return null;
473
61
  }
474
- }
475
- } catch (e) {
476
- // silently fail
62
+ })();
477
63
  }
478
- return null;
64
+ return _learningServicePromise;
479
65
  }
480
66
 
67
+
481
68
  const router = safeRequire(path.join(helpersDir, 'router.cjs'));
482
69
  const session = safeRequire(path.join(helpersDir, 'session.cjs'));
483
70
  const memory = safeRequire(path.join(helpersDir, 'memory.cjs'));
@@ -487,331 +74,6 @@ const intelligence = safeRequire(path.join(helpersDir, 'intelligence.cjs'));
487
74
  // then used by pre-task / post-task to bridge into the hook registry (Tasks 26, 39).
488
75
  let _hooksModule = null;
489
76
 
490
- // ── MicroAgent Trigger Scanner (Task 32) ────────────────────────────────────
491
- function _triggerExtractYamlValue(raw) {
492
- var v = raw.trim();
493
- if (v.startsWith('"') && v.endsWith('"')) {
494
- // YAML double-quoted: unescape \\ → \ so regex patterns like "\\b" become \b (word boundary)
495
- v = v.slice(1, -1).replace(/\\\\/g, '\\');
496
- } else if (v.startsWith("'") && v.endsWith("'")) {
497
- v = v.slice(1, -1);
498
- }
499
- return v;
500
- }
501
-
502
- function _triggerFinalize(partial, agentSlug) {
503
- return { pattern: partial.pattern, mode: partial.mode || 'inject', priority: partial.priority || 0, agentSlug: agentSlug };
504
- }
505
-
506
- function _triggerExtractFromFrontmatter(content, agentSlug) {
507
- var fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
508
- if (!fmMatch) return [];
509
- var block = fmMatch[1];
510
- var triggers = [];
511
- var lines = block.split('\n');
512
- var inTriggers = false;
513
- var cur = null;
514
- for (var i = 0; i < lines.length; i++) {
515
- var line = lines[i];
516
- var trimmed = line.trim();
517
- var indent = line.length - line.trimStart().length;
518
- if (trimmed === 'triggers:' || trimmed.startsWith('triggers:')) { inTriggers = true; continue; }
519
- if (inTriggers && indent === 0 && /^[a-zA-Z]/.test(trimmed)) {
520
- inTriggers = false;
521
- if (cur && cur.pattern) triggers.push(_triggerFinalize(cur, agentSlug));
522
- cur = null; continue;
523
- }
524
- if (!inTriggers) continue;
525
- if (trimmed.startsWith('- pattern:')) {
526
- if (cur && cur.pattern) triggers.push(_triggerFinalize(cur, agentSlug));
527
- cur = { pattern: _triggerExtractYamlValue(trimmed.replace(/^- pattern:\s*/, '')), agentSlug: agentSlug };
528
- } else if (cur && trimmed.startsWith('mode:')) {
529
- var mv = _triggerExtractYamlValue(trimmed.replace(/^mode:\s*/, ''));
530
- if (mv === 'inject' || mv === 'takeover') cur.mode = mv;
531
- } else if (cur && trimmed.startsWith('priority:')) {
532
- var pv = parseInt(trimmed.replace(/^priority:\s*/, ''), 10);
533
- if (!isNaN(pv)) cur.priority = pv;
534
- }
535
- }
536
- if (cur && cur.pattern) triggers.push(_triggerFinalize(cur, agentSlug));
537
- return triggers;
538
- }
539
-
540
- function _triggerCollectMdFiles(dir) {
541
- var results = [];
542
- try {
543
- var entries = fs.readdirSync(dir);
544
- for (var i = 0; i < entries.length; i++) {
545
- var full = path.join(dir, entries[i]);
546
- try {
547
- var st = fs.lstatSync(full);
548
- if (st.isDirectory()) results = results.concat(_triggerCollectMdFiles(full));
549
- else if (entries[i].endsWith('.md')) results.push(full);
550
- } catch (e) {}
551
- }
552
- } catch (e) {}
553
- return results;
554
- }
555
-
556
- function _triggerBuildIndex(agentDir) {
557
- var patterns = [];
558
- var files = _triggerCollectMdFiles(agentDir);
559
- for (var i = 0; i < files.length; i++) {
560
- var content;
561
- try { content = fs.readFileSync(files[i], 'utf-8'); } catch (e) { continue; }
562
- var slug = files[i].split('/').pop().replace(/\.md$/i, '').toLowerCase().replace(/[^a-z0-9-]/g, '-');
563
- patterns = patterns.concat(_triggerExtractFromFrontmatter(content, slug));
564
- }
565
- return patterns;
566
- }
567
-
568
- function scanMicroAgentTriggers(prompt) {
569
- if (!prompt || typeof prompt !== 'string') return { matches: [], injectAgents: [] };
570
- var indexPath = path.join(CWD, '.monomind', 'trigger-index.json');
571
- var agentDir = path.join(CWD, '.claude', 'agents');
572
- var patterns = [];
573
- var cacheLoaded = false;
574
-
575
- // Load cached index if fresh (< 1 hour)
576
- try {
577
- if (fs.existsSync(indexPath)) {
578
- var idx = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
579
- var age = Date.now() - new Date(idx.builtAt || 0).getTime();
580
- if (age < 3600000 && Array.isArray(idx.patterns)) {
581
- patterns = idx.patterns;
582
- cacheLoaded = true; // valid even when empty (no triggers defined)
583
- }
584
- }
585
- } catch (e) {}
586
-
587
- // Rebuild only when cache is missing or stale — not when it's a valid empty result
588
- if (!cacheLoaded) {
589
- patterns = _triggerBuildIndex(agentDir);
590
- try {
591
- fs.mkdirSync(path.join(CWD, '.monomind'), { recursive: true });
592
- fs.writeFileSync(indexPath, JSON.stringify({ patterns: patterns, builtAt: new Date().toISOString(), totalAgentsScanned: patterns.length }));
593
- } catch (e) {}
594
- }
595
-
596
- // Sort by descending priority
597
- patterns.sort(function(a, b) { return (b.priority || 0) - (a.priority || 0); });
598
-
599
- // Apply patterns
600
- var matches = [];
601
- var seen = {};
602
- for (var i = 0; i < patterns.length; i++) {
603
- var p = patterns[i];
604
- if (p.mode !== 'inject' && p.mode !== 'takeover') continue;
605
- if (seen[p.agentSlug]) continue;
606
- try {
607
- var re = new RegExp(p.pattern, 'i');
608
- var m = re.exec(prompt);
609
- if (m) {
610
- seen[p.agentSlug] = true;
611
- matches.push({ agentSlug: p.agentSlug, mode: p.mode, matchedText: m[0] });
612
- if (p.mode === 'takeover') {
613
- return { matches: matches, takeoverAgent: p.agentSlug, injectAgents: [] };
614
- }
615
- }
616
- } catch (e) {}
617
- }
618
- return { matches: matches, injectAgents: matches.map(function(m) { return m.agentSlug; }) };
619
- }
620
-
621
- // ── Task 28: Knowledge Base — inline CJS search + auto-indexer ─────────────
622
- //
623
- // Purpose: give KnowledgeRetriever a real search function and pre-populate
624
- // the knowledge store with project documents (CLAUDE.md, todo.md, etc.) so
625
- // retrieveForTask() actually returns useful context on session restore.
626
- // No compiled deps required — reads/writes JSONL directly.
627
-
628
- /**
629
- * Build a simple keyword-overlap search function over chunks.jsonl.
630
- * Returns results sorted by descending score; compatible with SearchFn signature.
631
- */
632
- var _KNOWLEDGE_STOPWORDS = new Set(['the','and','or','but','if','in','on','to','is','it','be','do','of','for','not','at','by','as','we','us','an','a','i']);
633
-
634
- function _buildKnowledgeSearchFn(knowledgeDir) {
635
- return async function(query, opts) {
636
- var chunksFile = path.join(knowledgeDir, 'chunks.jsonl');
637
- if (!fs.existsSync(chunksFile)) return [];
638
- var lines;
639
- try {
640
- lines = fs.readFileSync(chunksFile, 'utf-8').trim().split('\n').filter(Boolean);
641
- } catch (e) { return []; }
642
-
643
- var ns = (opts && opts.namespace) || null;
644
- var limit = (opts && opts.limit) || 10;
645
- var minScore = (opts && opts.minScore != null) ? opts.minScore : 0.3;
646
- var queryTerms = query.toLowerCase().split(/\s+/).filter(function(t) { return t.length >= 2 && !_KNOWLEDGE_STOPWORDS.has(t); });
647
- if (queryTerms.length === 0) return [];
648
-
649
- var results = [];
650
- for (var i = 0; i < lines.length; i++) {
651
- try {
652
- var chunk = JSON.parse(lines[i]);
653
- if (ns && chunk.namespace !== ns) continue;
654
- var textLower = (chunk.text || '').toLowerCase();
655
- var matchCount = queryTerms.filter(function(t) { return textLower.includes(t); }).length;
656
- var score = matchCount / queryTerms.length;
657
- if (score >= minScore) {
658
- results.push({ key: chunk.chunkId, value: chunk.text, score: score, metadata: chunk.metadata || {} });
659
- }
660
- } catch (e) {}
661
- }
662
- results.sort(function(a, b) { return b.score - a.score; });
663
- return results.slice(0, limit);
664
- };
665
- }
666
-
667
- /**
668
- * Index project knowledge sources into chunks.jsonl.
669
- * Skips re-indexing if content hasn't changed (hash-gated).
670
- * Returns the number of new chunks written.
671
- */
672
- function _autoIndexKnowledge(knowledgeDir) {
673
- var crypto = require('crypto');
674
- var sources = [
675
- { filePath: path.join(CWD, 'CLAUDE.md'), label: 'project-instructions' },
676
- { filePath: path.join(CWD, 'docs/todo.md'), label: 'project-todo' },
677
- { filePath: path.join(CWD, 'CLAUDE.local.md'), label: 'local-instructions' },
678
- ];
679
-
680
- // Compute a combined hash of all source file sizes (fast proxy for content change)
681
- var hashInput = '';
682
- for (var i = 0; i < sources.length; i++) {
683
- try {
684
- if (fs.existsSync(sources[i].filePath)) {
685
- var st = fs.statSync(sources[i].filePath);
686
- hashInput += sources[i].filePath + ':' + st.size + ':' + st.mtimeMs + ';';
687
- }
688
- } catch (e) {}
689
- }
690
- // Include monograph graph build time in hash so re-index happens after graph rebuild
691
- try {
692
- var statsForHash = JSON.parse(fs.readFileSync(path.join(CWD, '.monomind', 'graph', 'stats.json'), 'utf-8'));
693
- hashInput += 'monograph:' + (statsForHash.builtAt || 0) + ';';
694
- } catch(e) {}
695
-
696
- var contentHash = crypto.createHash('md5').update(hashInput).digest('hex');
697
-
698
- var chunksFile = path.join(knowledgeDir, 'chunks.jsonl');
699
- var hashFile = path.join(knowledgeDir, '.index-hash');
700
- var existingHash = '';
701
- try { existingHash = fs.readFileSync(hashFile, 'utf-8').trim(); } catch (e) {}
702
-
703
- // Nothing changed — skip re-index
704
- var existingChunkCount = 0;
705
- try { if (fs.existsSync(chunksFile)) { existingChunkCount = fs.readFileSync(chunksFile, 'utf-8').trim().split('\n').filter(Boolean).length; } } catch (e) {}
706
- if (existingHash === contentHash && existingChunkCount > 0) return 0;
707
-
708
- // Build new chunks
709
- var newLines = [];
710
- for (var si = 0; si < sources.length; si++) {
711
- var src = sources[si];
712
- try {
713
- if (!fs.existsSync(src.filePath)) continue;
714
- var content = fs.readFileSync(src.filePath, 'utf-8');
715
- // Split on blank lines or markdown headers (## / ###)
716
- var sections = content.split(/\n{2,}|\n(?=#{1,3} )/);
717
- for (var ci = 0; ci < sections.length; ci++) {
718
- var text = sections[ci].trim();
719
- if (text.length < 40 || text.length > 3000) continue;
720
- var chunkId = crypto.createHash('md5').update(src.filePath + ':' + ci).digest('hex').slice(0, 16);
721
- newLines.push(JSON.stringify({
722
- chunkId: chunkId,
723
- namespace: 'knowledge:shared',
724
- text: text,
725
- metadata: { filePath: src.filePath, label: src.label, chunkIndex: ci }
726
- }));
727
- }
728
- } catch (e) {}
729
- }
730
-
731
- // Inject monograph graph summary as a knowledge chunk.
732
- // Reads from .monomind/monograph.db (SQLite, source of truth) and falls
733
- // back to the legacy .monomind/graph/{stats,graph}.json pair only when
734
- // present (older installs).
735
- try {
736
- var mgDbPath2 = path.join(CWD, '.monomind', 'monograph.db');
737
- var legacyStats2 = path.join(CWD, '.monomind', 'graph', 'stats.json');
738
- var legacyGraph2 = path.join(CWD, '.monomind', 'graph', 'graph.json');
739
-
740
- var summaryText = null;
741
- var summaryMeta = {};
742
-
743
- if (fs.existsSync(mgDbPath2)) {
744
- try {
745
- var sumDb = _openMonographDb();
746
- if (sumDb) {
747
- try {
748
- var nodeC = sumDb.prepare('SELECT COUNT(*) AS c FROM nodes').get().c;
749
- var edgeC = sumDb.prepare('SELECT COUNT(*) AS c FROM edges').get().c;
750
- var topNodes2 = sumDb.prepare(
751
- 'SELECT n.name, n.label, n.file_path, ' +
752
- '(SELECT COUNT(*) FROM edges WHERE source_id=n.id OR target_id=n.id) AS deg ' +
753
- 'FROM nodes n WHERE n.file_path IS NOT NULL AND n.file_path != "" ORDER BY deg DESC LIMIT 15'
754
- ).all();
755
- var typeRows = sumDb.prepare(
756
- 'SELECT label, COUNT(*) AS c FROM nodes GROUP BY label ORDER BY c DESC LIMIT 8'
757
- ).all();
758
- var typeStr = typeRows.map(function(r) { return r.label + ':' + r.c; }).join(', ');
759
- summaryText = [
760
- 'MONOGRAPH KNOWLEDGE GRAPH SUMMARY',
761
- 'Source: monograph.db | Nodes: ' + nodeC + ' | Edges: ' + edgeC,
762
- '',
763
- 'TOP GOD NODES (highest connectivity — start exploration here):',
764
- topNodes2.map(function(n) {
765
- return ' ' + n.name + ' [' + n.label + '] — ' + (n.file_path || '') + ' (degree: ' + n.deg + ')';
766
- }).join('\n'),
767
- '',
768
- 'NODE TYPE DISTRIBUTION: ' + typeStr,
769
- '',
770
- 'Before grepping or globbing, prefer:',
771
- ' mcp__monomind__monograph_suggest({ task: "<your task>" }) — ranked relevant files',
772
- ' mcp__monomind__monograph_query({ q: "<symbol|keyword>" }) — BM25 search with file:line',
773
- ' mcp__monomind__monograph_impact({ name: "<file>" }) — upstream + downstream blast radius',
774
- ].join('\n');
775
- summaryMeta = { label: 'monograph-graph-summary', source: 'monograph.db', nodes: nodeC, edges: edgeC };
776
- } catch (e) { /* keep summaryText if partial */ }
777
- }
778
- } catch (e) { /* fall through to legacy */ }
779
- }
780
-
781
- if (!summaryText && fs.existsSync(legacyStats2) && fs.existsSync(legacyGraph2)) {
782
- try {
783
- var lStats = JSON.parse(fs.readFileSync(legacyStats2, 'utf-8'));
784
- var lGraphStat = fs.statSync(legacyGraph2);
785
- if (lGraphStat.size < 10 * 1024 * 1024) {
786
- var lGraph = JSON.parse(fs.readFileSync(legacyGraph2, 'utf-8'));
787
- var lNodes = Array.isArray(lGraph.nodes) ? lGraph.nodes : [];
788
- summaryText = 'MONOGRAPH KNOWLEDGE GRAPH SUMMARY (legacy JSON)\n' +
789
- 'Nodes: ' + (lStats.nodes || lNodes.length) + ' | Edges: ' + (lStats.edges || 0) + '\n' +
790
- 'Use mcp__monomind__monograph_suggest to find files relevant to your task.';
791
- summaryMeta = { label: 'monograph-graph-summary', source: 'legacy-json', builtAt: lStats.builtAt };
792
- }
793
- } catch (e) { /* ignore */ }
794
- }
795
-
796
- if (summaryText) {
797
- var chunkId = crypto.createHash('md5').update('monograph-graph-summary').digest('hex').slice(0, 16);
798
- newLines.push(JSON.stringify({
799
- chunkId: chunkId,
800
- namespace: 'knowledge:shared',
801
- text: summaryText,
802
- metadata: summaryMeta
803
- }));
804
- }
805
- } catch (e) { /* graph not available yet, skip */ }
806
-
807
- try {
808
- fs.mkdirSync(knowledgeDir, { recursive: true });
809
- fs.writeFileSync(chunksFile, newLines.length > 0 ? newLines.join('\n') + '\n' : '', 'utf-8');
810
- fs.writeFileSync(hashFile, contentHash, 'utf-8');
811
- } catch (e) {}
812
- return newLines.length;
813
- }
814
-
815
77
  // ── Intelligence timeout protection (fixes #1530, #1531) ───────────────────
816
78
  var INTELLIGENCE_TIMEOUT_MS = 1500;
817
79
  function runWithTimeout(fn, label) {
@@ -902,1510 +164,89 @@ async function main() {
902
164
  return false;
903
165
  }
904
166
 
167
+ // Build shared hook context — passed to extracted handler modules so they
168
+ // don't need to capture main()-scoped or module-scoped variables via closure.
169
+ var hCtx = {
170
+ hookInput: hookInput,
171
+ toolInput: toolInput,
172
+ toolName: toolName,
173
+ prompt: prompt,
174
+ args: args,
175
+ CWD: CWD,
176
+ session: session,
177
+ router: router,
178
+ intelligence: intelligence,
179
+ getLearningService: getLearningService,
180
+ isSimpleCommand: isSimpleCommand,
181
+ // Module-level singleton (populated by session-restore handler)
182
+ get _hooksModule() { return _hooksModule; },
183
+ set _hooksModule(v) { _hooksModule = v; },
184
+ // Utility functions
185
+ _recordRecentEdit: _recordRecentEdit,
186
+ _getRecentEdits: _getRecentEdits,
187
+ _findAffectedTests: _findAffectedTests,
188
+ _recordHookLatency: _recordHookLatency,
189
+ _getBudgetStatus: _getBudgetStatus,
190
+ _injectCompactGraphMap: _injectCompactGraphMap,
191
+ _maybeRebuildMonograph: _maybeRebuildMonograph,
192
+ _buildKnowledgeSearchFn: _buildKnowledgeSearchFn,
193
+ getMonographSuggestions: getMonographSuggestions,
194
+ getMonographNeighbors: getMonographNeighbors,
195
+ runWithTimeout: runWithTimeout,
196
+ safeRequire: safeRequire,
197
+ scanMicroAgentTriggers: scanMicroAgentTriggers,
198
+ _recordGraphTelemetry: _recordGraphTelemetry,
199
+ _recordDecisionMarkers: _recordDecisionMarkers,
200
+ _recordToolCall: _recordToolCall,
201
+ _openMonographDb: _openMonographDb,
202
+ _requireMonograph: _requireMonograph,
203
+ _triggerExtractYamlValue: _triggerExtractYamlValue,
204
+ _triggerFinalize: _triggerFinalize,
205
+ _triggerExtractFromFrontmatter: _triggerExtractFromFrontmatter,
206
+ _triggerCollectMdFiles: _triggerCollectMdFiles,
207
+ _triggerBuildIndex: _triggerBuildIndex,
208
+ _autoIndexKnowledge: _autoIndexKnowledge,
209
+ helpersDir: helpersDir,
210
+ fs: fs,
211
+ path: path,
212
+ };
213
+
905
214
  const handlers = {
906
215
  'route': async () => {
907
- // For slash commands and single-action invocations: skip routing panel output
908
- // but still write last-route.json so the statusline reflects the current action.
909
- if (isSimpleCommand(prompt)) {
910
- try {
911
- var cmdLabel = (typeof prompt === 'string' && prompt.trim().startsWith('/'))
912
- ? prompt.trim().split(/\s+/)[0] // e.g. "/ts"
913
- : (hookInput.commandName || hookInput.command_name || 'command');
914
- var routeDir = path.join(CWD, '.monomind');
915
- fs.mkdirSync(routeDir, { recursive: true });
916
- fs.writeFileSync(
917
- path.join(routeDir, 'last-route.json'),
918
- JSON.stringify({
919
- agent: cmdLabel,
920
- confidence: 1.0,
921
- reason: 'predefined command — no routing needed',
922
- semanticRouting: false,
923
- updatedAt: new Date().toISOString(),
924
- }),
925
- 'utf-8'
926
- );
927
- } catch (e) { /* non-fatal */ }
928
- return;
929
- }
930
-
931
- if (intelligence && intelligence.getContext) {
932
- try {
933
- const ctx = intelligence.getContext(prompt);
934
- if (ctx) console.log(ctx);
935
- } catch (e) { /* non-fatal */ }
936
- }
937
- if (router && (router.routeTaskSemantic || router.routeTask)) {
938
- const routeFn = router.routeTaskSemantic || router.routeTask;
939
- var result = await Promise.resolve(routeFn(prompt));
940
-
941
- // Graph-fallback override: when the router picked a low-confidence
942
- // non-dev specialist (marketing slugs etc) but monograph has a strong
943
- // graph match for the prompt, derive the agent from the top file's
944
- // label instead. Stops "improve the system" → China E-Commerce.
945
- try {
946
- // Don't override when the prompt has obvious non-dev keywords —
947
- // marketing/sales/finance asks SHOULD route to those specialists.
948
- var nonDevPrompt = /\b(marketing|advertis|seo|tiktok|instagram|linkedin|sales|customer|brand|blog post|content strategy|copy(?:writ|writing)|pitch|investor|hr|recruit|legal|compliance|tax|invoice|accounting|onboarding|design syst|figma|user research|persona)\b/i.test(prompt);
949
-
950
- var devAgents = /^(coder|tester|reviewer|planner|researcher|system-architect|backend-dev|backend-architect|mobile-dev|ml-developer|cicd-engineer|api-docs|code-analyzer|production-validator|Technical Writer)$/i;
951
- var pickedDev = devAgents.test(String(result.agent || '').trim()) ||
952
- devAgents.test(String(result.agentSlug || '').trim());
953
-
954
- var resConf = (result.confidence != null ? result.confidence : 0);
955
- var resReason = String(result.reason || '');
956
- var fromKeywordStage = resReason.indexOf('Keyword 2-stage') !== -1;
957
- var promptIsDevish = /\b(improve|refactor|fix|bug|optimi[sz]e|implement|build|debug|deploy|test|feature|system|performance|architecture|memory|hook|graph|statusline|monograph|api|cli|skill|hooks|agent|workflow|init|module|package|registry|server|client|route|handler)\b/i.test(prompt);
958
-
959
- var shouldOverride = !nonDevPrompt && (
960
- (!pickedDev && resConf < 0.85) ||
961
- (fromKeywordStage && promptIsDevish)
962
- );
963
- if (shouldOverride) {
964
- var topGraph = getMonographSuggestions(prompt, 1)[0];
965
- if (topGraph) {
966
- var agent = 'coder';
967
- var file = (topGraph.file || '').toLowerCase();
968
- // Test files
969
- if (/\.(test|spec)\./.test(file) || file.includes('__tests__')) agent = 'tester';
970
- // Architecture/system docs → architect
971
- else if (/(architect|adr-|design-doc|rfc-)/.test(file)) agent = 'system-architect';
972
- // Pure docs → tech writer
973
- else if (file.endsWith('readme.md') || file.startsWith('docs/') || /\/docs\//.test(file)) agent = 'Technical Writer';
974
- // Other .md (skills, agents, configs) → coder (they're code-adjacent)
975
- else if (file.endsWith('.md')) agent = 'coder';
976
- // Class/Interface → architect
977
- else if (topGraph.label === 'Class' || topGraph.label === 'Interface') agent = 'system-architect';
978
- // Functions, files, methods → coder
979
- else agent = 'coder';
980
- // Scale confidence by graph degree: well-connected nodes are stronger anchors.
981
- var topDeg = topGraph.deg || 0;
982
- var graphConf = topDeg > 30 ? 0.80 : (topDeg > 10 ? 0.75 : 0.70);
983
- result = Object.assign({}, result, {
984
- agent: agent,
985
- agentSlug: agent,
986
- confidence: graphConf,
987
- reason: 'Graph fallback: top file ' + (topGraph.name || '').substring(0, 30) + ' [' + topGraph.label + '] deg=' + topDeg,
988
- specificAgents: [],
989
- extrasMatches: [],
990
- });
991
- }
992
- }
993
- } catch (e) {}
994
-
995
- var output = [];
996
- output.push('[INFO] Routing task: ' + (prompt.substring(0, 80) || '(no prompt)'));
997
- output.push('');
998
- // Suppress the agent recommendation panel for low-confidence routes on
999
- // short prompts — the recommendation is almost always wrong (e.g.
1000
- // "what else can we do" → marketing → China E-Commerce). Saves ~150
1001
- // tokens per prompt. Skill matches and specific agents still render
1002
- // below when confidence is decent.
1003
- var conf = result.confidence != null ? result.confidence : 0;
1004
- var promptShort = (prompt || '').trim().length < 60;
1005
- var lowConf = conf < 0.70;
1006
- var suppressPanel = lowConf && promptShort;
1007
- if (!suppressPanel) {
1008
- output.push('+------------- monomind | Primary Recommendation --------------+');
1009
- output.push('| Agent: ' + (result.agent || 'unknown').substring(0, 54).padEnd(54) + '|');
1010
- output.push('| Confidence: ' + ((result.confidence != null ? (result.confidence * 100).toFixed(1) : '?') + '%').padEnd(49) + '|');
1011
- output.push('| Reason: ' + (result.reason || '').substring(0, 53).padEnd(53) + '|');
1012
- output.push('+--------------------------------------------------------------+');
1013
- }
1014
-
1015
- // ── Persist routing result for statusline display ─────────────
1016
- try {
1017
- var routeDir = path.join(CWD, '.monomind');
1018
- fs.mkdirSync(routeDir, { recursive: true });
1019
- // Always use the resolved agent name — never persist "extras"
1020
- var resolvedAgent = result.agent;
1021
- if (!resolvedAgent || resolvedAgent === 'extras') {
1022
- var topExtra = result.extrasMatches && result.extrasMatches[0];
1023
- resolvedAgent = topExtra ? topExtra.name : 'Specialist Agent';
1024
- }
1025
- var routePayload = {
1026
- agent: resolvedAgent,
1027
- agentSlug: result.agentSlug || null,
1028
- confidence: result.confidence,
1029
- reason: result.reason,
1030
- semanticRouting: result.semanticRouting || false,
1031
- llmRouting: result.llmRouting || false,
1032
- updatedAt: new Date().toISOString(),
1033
- };
1034
- if (result.extrasMatches && result.extrasMatches.length > 0) {
1035
- routePayload.extrasMatches = result.extrasMatches.map(function(e) {
1036
- return { name: e.name, slug: e.slug, category: e.category };
1037
- });
1038
- }
1039
- fs.writeFileSync(
1040
- path.join(routeDir, 'last-route.json'),
1041
- JSON.stringify(routePayload),
1042
- 'utf-8'
1043
- );
1044
- } catch (e) { /* non-fatal */ }
1045
-
1046
- // ── Dev skill suggestions ──────────────────────────────────────
1047
- var matches = result.skillMatches || [];
1048
- if (matches.length > 0) {
1049
- // Check for high-confidence auto-invoke: if top skill scored >= 3 keyword
1050
- // hits and is the dominant match, auto-invoke instead of just suggesting
1051
- var topMatch = matches[0];
1052
- var autoInvoke = false;
1053
- if (topMatch && topMatch.score >= 3 && matches.length <= 2) {
1054
- autoInvoke = true;
1055
- } else if (topMatch && topMatch.score >= 2 && matches.length === 1 && (result.confidence ?? 0) < 0.7) {
1056
- // Single strong skill match with weak agent routing = skill should take over
1057
- autoInvoke = true;
1058
- }
1059
-
1060
- if (autoInvoke) {
1061
- output.push('');
1062
- output.push('+======== SKILL AUTO-ACTIVATED (high confidence match) ========+');
1063
- output.push('| ' + topMatch.invoke.substring(0, 61).padEnd(61) + '|');
1064
- output.push('| INSTRUCTION: Invoke ' + topMatch.invoke.substring(0, 41).padEnd(41) + '|');
1065
- output.push('| BEFORE responding. This skill matched with very high |');
1066
- output.push('| confidence — do not skip it. |');
1067
- output.push('+==============================================================+');
1068
- } else {
1069
- output.push('');
1070
- if ((result.confidence ?? 0) < 0.8) {
1071
- output.push('+----------- Skill Suggestions (pick one if relevant) ---------+');
1072
- output.push('| No strong primary match — here are the best skill candidates |');
1073
- } else {
1074
- output.push('+----------- Matching Skills (invoke via Skill tool) ----------+');
1075
- }
1076
- matches.forEach(function(m, i) {
1077
- var label = (i + 1) + '. ' + m.skill;
1078
- var desc = (m.description || '').substring(0, 30);
1079
- var line = '| ' + label.substring(0, 30).padEnd(30) + desc.padEnd(30) + ' |';
1080
- output.push(line);
1081
- output.push('| invoke: ' + m.invoke.substring(0, 51).padEnd(51) + '|');
1082
- });
1083
- output.push('+--------------------------------------------------------------+');
1084
- if ((result.confidence ?? 0) < 0.8) {
1085
- output.push('| To use a skill: call Skill("skill-name") before responding. |');
1086
- output.push('+--------------------------------------------------------------+');
1087
- }
1088
- }
1089
- }
1090
-
1091
- // ── Specific agent panel ──────────────────────────────────────────────────
1092
- // Skip entirely on suppressed (low-confidence + short) prompts.
1093
- var specificAgents = result.specificAgents || [];
1094
- if (specificAgents.length > 0 && !suppressPanel) {
1095
- output.push('');
1096
- var saHdr = '------- Specific Agents (' + specificAgents.length + ' available) ';
1097
- output.push('+' + saHdr + '-'.repeat(Math.max(1, 62 - saHdr.length)) + '+');
1098
- specificAgents.forEach(function(a, i) {
1099
- var label = (i + 1) + '. ' + a.label;
1100
- var note = (a.note || '').substring(0, 26);
1101
- output.push('| ' + label.substring(0, 33).padEnd(33) + note.padEnd(27) + ' |');
1102
- if (a.slug) {
1103
- output.push('| slug: ' + a.slug.substring(0, 52).padEnd(52) + ' |');
1104
- }
1105
- });
1106
- output.push('+--------------------------------------------------------------+');
1107
- output.push('| Use: Task({ subagent_type: "<slug>" }) or /specialagent |');
1108
- output.push('+--------------------------------------------------------------+');
1109
- }
1110
-
1111
- // ── Specialist agents (non-dev domain) — only shown when specificAgents panel wasn't shown ──
1112
- var extras = result.extrasMatches || [];
1113
- var specificAgentsShown = (result.specificAgents || []).length > 0;
1114
- if (extras.length > 0 && !specificAgentsShown && !suppressPanel) {
1115
- output.push('');
1116
- var spHdr = '------- Specialist Agents (' + extras.length + ' matched) ';
1117
- output.push('+' + spHdr + '-'.repeat(Math.max(1, 62 - spHdr.length)) + '+');
1118
- extras.slice(0, 5).forEach(function(e, i) {
1119
- var label = (i + 1) + '. ' + e.name;
1120
- var cat = '[' + e.category + ']';
1121
- output.push('| ' + label.substring(0, 44).padEnd(44) + cat.substring(0, 16).padEnd(16) + ' |');
1122
- output.push('| slug: ' + e.slug.substring(0, 52).padEnd(52) + ' |');
1123
- });
1124
- output.push('+--------------------------------------------------------------+');
1125
- output.push('| Use: Task({ subagent_type: "<slug>" }) or /specialagent |');
1126
- output.push('+--------------------------------------------------------------+');
1127
- }
1128
-
1129
- // ── MicroAgent Trigger Scan (Task 32) ──────────────────────────────
1130
- try {
1131
- var triggerResult = scanMicroAgentTriggers(typeof prompt === 'string' ? prompt : '');
1132
- if (triggerResult.matches.length > 0) {
1133
- output.push('');
1134
- if (triggerResult.takeoverAgent) {
1135
- var tAgent = triggerResult.takeoverAgent;
1136
- var tKw = triggerResult.matches[0].matchedText;
1137
- output.push('+============= MicroAgent TAKEOVER Detected ===================+');
1138
- output.push('| Specialist: ' + tAgent.substring(0, 49).padEnd(49) + '|');
1139
- output.push('| Keyword: ' + ('"' + tKw + '"').substring(0, 49).padEnd(49) + '|');
1140
- output.push('| Recommended: use this specialist instead of primary agent. |');
1141
- output.push('+==============================================================+');
1142
- } else {
1143
- output.push('+------- MicroAgent Specialists Triggered ---------------------+');
1144
- triggerResult.matches.forEach(function(m) {
1145
- var slug = m.agentSlug.substring(0, 37).padEnd(37);
1146
- var kw = ('(match: "' + m.matchedText + '")').substring(0, 21).padEnd(21);
1147
- output.push('| + ' + slug + kw + ' |');
1148
- });
1149
- output.push('+--------------------------------------------------------------+');
1150
- }
1151
- // Persist trigger matches alongside route result
1152
- try {
1153
- var routeFile = path.join(CWD, '.monomind', 'last-route.json');
1154
- var existing = JSON.parse(fs.readFileSync(routeFile, 'utf-8'));
1155
- existing.microAgents = { injectAgents: triggerResult.injectAgents || [], takeoverAgent: triggerResult.takeoverAgent || null };
1156
- fs.writeFileSync(routeFile, JSON.stringify(existing), 'utf-8');
1157
- } catch (e) {}
1158
- }
1159
- } catch (e) { /* non-fatal */ }
1160
-
1161
- console.log(output.join('\n'));
1162
-
1163
- // Record any decision markers in this prompt (auto-ADR pipeline).
1164
- try { _recordDecisionMarkers(prompt); } catch (e) {}
1165
-
1166
- // Cost budget — emit amber/red banner when approaching limit.
1167
- try {
1168
- var budget = _getBudgetStatus();
1169
- if (budget && budget.alert) {
1170
- var tunedNote = budget.autoTuned ? ' (auto-tuned)' : '';
1171
- if (budget.spike && !budget.breached) {
1172
- console.log('[BUDGET_SPIKE] Today $' + budget.todayCost.toFixed(2) + ' is >2x your rolling daily avg. Unusual spend — review .monomind/metrics/token-summary.json.');
1173
- } else if (budget.breached) {
1174
- console.log('[BUDGET_BREACHED] Daily $' + budget.todayCost.toFixed(2) + '/$' + budget.dailyLimit + ' (' + budget.dailyPct + '%) · Monthly $' + budget.monthCost.toFixed(2) + '/$' + budget.monthlyLimit + ' (' + budget.monthlyPct + '%)' + tunedNote + '. Switch to Haiku with /model haiku or edit .monomind/budget.json.');
1175
- } else {
1176
- console.log('[BUDGET_ALERT] Daily ' + budget.dailyPct + '% of $' + budget.dailyLimit + ' · Monthly ' + budget.monthlyPct + '% of $' + budget.monthlyLimit + tunedNote + '.');
1177
- }
1178
- }
1179
- } catch (e) {}
1180
-
1181
- // Inject monograph hint for complex tasks.
1182
- // Source of truth is .monomind/monograph.db (SQLite). Legacy stats.json
1183
- // is no longer written by the build, so it is checked only as a fallback.
1184
- try {
1185
- var monographDb = path.join(CWD, '.monomind', 'monograph.db');
1186
- var legacyStats = path.join(CWD, '.monomind', 'graph', 'stats.json');
1187
- var nodeCount = 0;
1188
- if (fs.existsSync(monographDb)) {
1189
- try {
1190
- var hintDb = _openMonographDb();
1191
- if (hintDb) {
1192
- nodeCount = hintDb.prepare('SELECT COUNT(*) AS c FROM nodes').get().c;
1193
- }
1194
- } catch (e) { /* ignore — fall back to legacy */ }
1195
- }
1196
- if (nodeCount === 0 && fs.existsSync(legacyStats)) {
1197
- try {
1198
- var gStats = JSON.parse(fs.readFileSync(legacyStats, 'utf-8'));
1199
- nodeCount = gStats.nodes || 0;
1200
- } catch (e) { /* ignore */ }
1201
- }
1202
- if (nodeCount > 100) {
1203
- // Pre-resolve top-5 relevant files for the user's prompt — the LLM
1204
- // sees the answer inline instead of being told to call a tool.
1205
- var suggestions = getMonographSuggestions(prompt, 5);
1206
-
1207
- // Boost recently-edited files to the top of pre-resolve suggestions.
1208
- // Even when the FTS index hasn't caught up to the latest edits, the
1209
- // LLM should see the files it just modified as the primary context.
1210
- try {
1211
- var recentEditsForRoute = _getRecentEdits();
1212
- if (recentEditsForRoute.length > 0) {
1213
- // Extract prompt keywords for relevance gating
1214
- var promptWords = (prompt || '').toLowerCase().match(/[a-z][a-z0-9_-]{2,}/g) || [];
1215
- var promptWordSet = {};
1216
- for (var pw = 0; pw < promptWords.length; pw++) promptWordSet[promptWords[pw]] = 1;
1217
-
1218
- var existingFiles = {};
1219
- for (var se = 0; se < suggestions.length; se++) existingFiles[suggestions[se].file || ''] = 1;
1220
-
1221
- var editBoosts = [];
1222
- for (var re = 0; re < recentEditsForRoute.length && editBoosts.length < 2; re++) {
1223
- var reFile = recentEditsForRoute[re].file;
1224
- // Skip if already in suggestions
1225
- if (existingFiles[reFile]) continue;
1226
- var reName = path.basename(reFile, path.extname(reFile)).toLowerCase();
1227
- // Only boost if filename shares a keyword with the prompt OR the edit is very recent (<3 min)
1228
- var veryRecent = (Date.now() - recentEditsForRoute[re].editedAt) < 3 * 60 * 1000;
1229
- var matches = promptWordSet[reName] || veryRecent;
1230
- if (matches) {
1231
- editBoosts.push({ name: path.basename(reFile), label: 'File', file: reFile, deg: 0, _editBoost: true });
1232
- }
1233
- }
1234
- if (editBoosts.length > 0) {
1235
- suggestions = editBoosts.concat(suggestions).slice(0, 5);
1236
- }
1237
- }
1238
- } catch (e) { /* non-fatal */ }
1239
-
1240
- if (suggestions.length > 0) {
1241
- console.log('\n[MONOGRAPH] ' + nodeCount + ' nodes indexed. Top files for this task (pre-resolved from graph):');
1242
- for (var si = 0; si < suggestions.length; si++) {
1243
- var s = suggestions[si];
1244
- var editTag = s._editBoost ? ' ✎' : '';
1245
- console.log(' · ' + s.name + ' [' + s.label + '] — ' + (s.file || '') + (s.deg ? ' (deg ' + s.deg + ')' : '') + editTag);
1246
- }
1247
- console.log(' Use mcp__monomind__monograph_query / monograph_impact for deeper drill-down.');
1248
- _recordGraphTelemetry('preresolve_hit');
1249
- } else {
1250
- console.log('\n[MONOGRAPH] ' + nodeCount + ' nodes indexed. Call mcp__monomind__monograph_suggest first to find relevant files without grepping.');
1251
- _recordGraphTelemetry('preresolve_miss');
1252
- }
1253
- }
1254
- } catch(e) {}
1255
-
1256
- // Swarm mode selection is available on-demand via /mastermind slash command.
1257
- } else {
1258
- console.log('[INFO] Router not available, using default routing');
1259
- }
1260
-
1261
- // Task 22: TeamRoutingModes — only log when an explicit swarm config is present
1262
- try {
1263
- var swarmCfgPath = path.join(CWD, '.monomind', 'swarm-config.json');
1264
- if (fs.existsSync(swarmCfgPath)) {
1265
- var topology22 = JSON.parse(fs.readFileSync(swarmCfgPath, 'utf-8')).topology || 'mesh';
1266
- var mode22 = topology22 === 'hierarchical' ? 'route' : 'coordinate';
1267
- console.log('[ROUTING_MODE] topology=' + topology22 + ' → mode=' + mode22);
1268
- }
1269
- } catch (e) { /* non-fatal */ }
1270
- },
1271
-
1272
- 'load-agent': () => {
1273
- // Load and print full agent text so Claude can adopt its identity
1274
- const slug = args.join(' ').trim() || (typeof prompt === 'string' ? prompt.trim() : '');
1275
- if (!router || !router.loadExtrasAgent) {
1276
- console.error('[ERROR] Router does not support loadExtrasAgent');
1277
- process.exit(1);
1278
- }
1279
- const agent = router.loadExtrasAgent(slug);
1280
- if (!agent) {
1281
- console.error('[ERROR] Extras agent not found: ' + slug);
1282
- console.error('Run: node .claude/helpers/router.cjs --load-agent <slug> to check available slugs');
1283
- process.exit(1);
1284
- }
1285
- console.log('=== AGENT ACTIVATED: ' + agent.name + ' [' + agent.category + '] ===');
1286
- console.log('');
1287
- console.log(agent.content);
1288
- console.log('');
1289
- console.log('=== END AGENT: ' + agent.name + ' ===');
1290
- console.log('INSTRUCTION: You are now ' + agent.name + '. Adopt the identity, tone, and expertise described above for the remainder of this task.');
1291
-
1292
- // Persist active agent for statusline
1293
- try {
1294
- var routeDir = path.join(CWD, '.monomind');
1295
- fs.mkdirSync(routeDir, { recursive: true });
1296
- fs.writeFileSync(
1297
- path.join(routeDir, 'last-route.json'),
1298
- JSON.stringify({
1299
- agent: agent.slug,
1300
- name: agent.name,
1301
- category: agent.category,
1302
- confidence: 1.0,
1303
- reason: 'manually activated via load-agent',
1304
- activated: true,
1305
- updatedAt: new Date().toISOString(),
1306
- }),
1307
- 'utf-8'
1308
- );
1309
- } catch (e) { /* non-fatal */ }
1310
- },
1311
-
1312
- 'list-extras': () => {
1313
- if (!router || !router.loadExtrasRegistry) {
1314
- console.error('[ERROR] Extras registry not available');
1315
- process.exit(1);
1316
- }
1317
- const registry = router.loadExtrasRegistry();
1318
- const category = args[0] || '';
1319
- const entries = category
1320
- ? registry.extras.filter(e => e.category === category)
1321
- : registry.extras;
1322
- const byCategory = {};
1323
- for (const e of entries) {
1324
- if (!byCategory[e.category]) byCategory[e.category] = [];
1325
- byCategory[e.category].push(e);
1326
- }
1327
- for (const [cat, agents] of Object.entries(byCategory)) {
1328
- console.log('\n[' + cat.toUpperCase() + ']');
1329
- for (const a of agents) {
1330
- console.log(' ' + a.slug.padEnd(45) + a.name);
1331
- }
1332
- }
1333
- console.log('\nTotal: ' + entries.length + ' extras agents');
1334
- },
1335
-
1336
- 'pre-bash': () => {
1337
- var _rawCmd = hookInput.command || prompt;
1338
- var rawCmdStr = (typeof _rawCmd === 'string' ? _rawCmd : String(_rawCmd || ''));
1339
- var cmd = rawCmdStr.toLowerCase();
1340
- var dangerous = ['rm -rf /', 'rm -rf/', 'format c:', 'del /s /q c:\\', ':(){:|:&};:'];
1341
- for (var i = 0; i < dangerous.length; i++) {
1342
- if (cmd.includes(dangerous[i])) {
1343
- console.error('[BLOCKED] Dangerous command detected: ' + dangerous[i]);
1344
- process.exit(1);
1345
- }
1346
- }
1347
-
1348
- // Intercept Bash-side grep/rg/find/ag — close the loophole where LLM
1349
- // bypasses the Grep tool by shelling out. Same monograph hint shape as pre-search.
1350
- try {
1351
- // Match: grep <flags> <pattern>, rg <flags> <pattern>, ag <pattern>, find . -name <pattern>
1352
- var grepMatch = rawCmdStr.match(/\b(?:grep|rg|ag)\b(?:\s+-[a-zA-Z]+)*\s+(?:"([^"]+)"|'([^']+)'|([^\s|;&<>]+))/);
1353
- var findMatch = rawCmdStr.match(/\bfind\b.*?-name\s+(?:"([^"]+)"|'([^']+)'|([^\s|;&<>]+))/);
1354
- var pattern = '';
1355
- if (grepMatch) {
1356
- pattern = grepMatch[1] || grepMatch[2] || grepMatch[3] || '';
1357
- _recordGraphTelemetry('bash_grep_call');
1358
- } else if (findMatch) {
1359
- pattern = findMatch[1] || findMatch[2] || findMatch[3] || '';
1360
- _recordGraphTelemetry('bash_find_call');
1361
- }
1362
- if (pattern && pattern.length >= 3) {
1363
- var sigB = 'Bash-grep:' + pattern.slice(0, 60);
1364
- var countB = _recordToolCall(sigB);
1365
- if (countB >= 3) {
1366
- console.log('[LOOP_DRIFT] You\'ve grepped "' + pattern.slice(0, 40) + '" ' + countB + 'x — switch to monograph_query.');
1367
- }
1368
- }
1369
- if (pattern && pattern.length >= 3) {
1370
- var clean = pattern.replace(/[\\^$.*+?()\[\]{}|]/g, ' ').trim();
1371
- if (clean.length >= 3) {
1372
- var hits = getMonographSuggestions(clean, 5);
1373
- if (hits.length > 0) {
1374
- _recordGraphTelemetry('graph_assist_search');
1375
- console.log('[MONOGRAPH_HIT] Graph has ' + hits.length + ' file(s) for "' + clean.slice(0, 40) + '" — consider monograph_query instead of shell grep:');
1376
- for (var j = 0; j < hits.length; j++) {
1377
- var h = hits[j];
1378
- console.log(' · ' + h.name + ' [' + h.label + '] — ' + (h.file || ''));
1379
- }
1380
- }
1381
- }
1382
- }
1383
- } catch (e) { /* non-fatal */ }
1384
-
1385
- console.log('[OK] Command validated');
1386
- },
1387
-
1388
- // Intercept Grep/Glob: run monograph_query on the same pattern first and
1389
- // surface graph hits so the LLM can read file:line directly without paying
1390
- // for a full Grep scan when the graph already knows the answer.
1391
- 'pre-search': () => {
1392
- try {
1393
- _recordGraphTelemetry(toolName === 'Grep' ? 'grep_call' : 'glob_call');
1394
- var pattern = toolInput.pattern || toolInput.path || '';
1395
- if (typeof pattern !== 'string' || pattern.length < 3) return;
1396
- var clean = pattern.replace(/[\\^$.*+?()\[\]{}|]/g, ' ').trim();
1397
- if (clean.length < 3) return;
1398
-
1399
- var sig = (toolName || 'Search') + ':' + clean.slice(0, 60);
1400
- var count = _recordToolCall(sig);
1401
- if (count >= 3) {
1402
- 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.');
1403
- }
1404
- var suggestions = getMonographSuggestions(clean, 5);
1405
- if (suggestions.length === 0) return;
1406
- // Successful intercept — count as a "graph assist" so the ratio reflects
1407
- // server-side wins, not just LLM-initiated MCP calls.
1408
- _recordGraphTelemetry('graph_assist_search');
1409
- console.log('[MONOGRAPH_HIT] Graph already knows ' + suggestions.length + ' file(s) matching "' + clean.slice(0, 40) + '":');
1410
- for (var i = 0; i < suggestions.length; i++) {
1411
- var s = suggestions[i];
1412
- console.log(' · ' + s.name + ' [' + s.label + '] — ' + (s.file || '') + ' (deg ' + s.deg + ')');
1413
- }
1414
- console.log(' Prefer mcp__monomind__monograph_query for symbol lookup (file:line, no scan).');
1415
- } catch (e) { /* non-fatal */ }
1416
- },
1417
-
1418
- // After Read: append a graph neighbor footer so the LLM gets free
1419
- // architectural context — what imports this file and what it imports.
1420
- 'post-read': () => {
1421
- try {
1422
- var filePath = toolInput.file_path || toolInput.path || '';
1423
- if (!filePath || typeof filePath !== 'string') return;
1424
- var n = getMonographNeighbors(filePath);
1425
- if (!n) return;
1426
- var parts = [];
1427
- if (n.importedBy.length > 0) parts.push('imported-by: ' + n.importedBy.slice(0, 4).join(', '));
1428
- if (n.imports.length > 0) parts.push('imports: ' + n.imports.slice(0, 4).join(', '));
1429
- if (parts.length === 0) return;
1430
- _recordGraphTelemetry('graph_assist_neighbors');
1431
- console.log('[MONOGRAPH_NEIGHBORS] ' + parts.join(' · '));
1432
- } catch (e) { /* non-fatal */ }
1433
- },
1434
-
1435
- // PostToolUse for monograph_* — increment graph usage counter for telemetry.
1436
- 'post-graph-tool': () => {
1437
- try {
1438
- if (toolName && toolName.indexOf('monograph_') !== -1) {
1439
- _recordGraphTelemetry('monograph_call');
1440
- }
1441
- } catch (e) {}
216
+ const h = require('./handlers/route-handler.cjs');
217
+ await h.handle(hCtx);
1442
218
  },
1443
219
 
1444
220
  'post-edit': async () => {
1445
- if (session && session.metric) {
1446
- try { session.metric('edits'); } catch (e) { /* no active session */ }
1447
- }
1448
- if (intelligence && intelligence.recordEdit) {
1449
- try {
1450
- var file = hookInput.file_path || toolInput.file_path
1451
- || process.env.TOOL_INPUT_file_path || args[0] || '';
1452
- intelligence.recordEdit(file);
1453
- } catch (e) { /* non-fatal */ }
1454
- }
1455
- // Track recently-edited files for compact injection and pre-resolve boosting.
1456
- try {
1457
- var editedForRecent = hookInput.file_path || toolInput.file_path
1458
- || process.env.TOOL_INPUT_file_path || args[0] || '';
1459
- if (editedForRecent) _recordRecentEdit(editedForRecent);
1460
- } catch (e) { /* non-fatal */ }
1461
- // Increment write counter and rebuild monograph when threshold hit.
1462
- _maybeRebuildMonograph();
1463
-
1464
- // Test feedback (detection-only): when editing a source file, list tests
1465
- // that import it so the LLM/user knows what to verify next.
1466
- try {
1467
- var editedFile = hookInput.file_path || toolInput.file_path
1468
- || process.env.TOOL_INPUT_file_path || args[0] || '';
1469
- if (editedFile && !editedFile.match(/\.(test|spec)\./) && !editedFile.includes('__tests__')) {
1470
- var affectedTests = _findAffectedTests(editedFile);
1471
- if (affectedTests.length > 0) {
1472
- console.log('[AFFECTED_TESTS] ' + affectedTests.length + ' test(s) cover this file:');
1473
- for (var ti = 0; ti < Math.min(5, affectedTests.length); ti++) {
1474
- console.log(' · ' + affectedTests[ti]);
1475
- }
1476
- }
1477
- }
1478
- } catch (e) {}
1479
- // ── Security-Sensitive File Auto-Alert ────────────────────────────────
1480
- // When editing auth, security, crypto, or env-related files, flag it
1481
- try {
1482
- var editFile = (hookInput.file_path || toolInput.file_path
1483
- || process.env.TOOL_INPUT_file_path || args[0] || '').toLowerCase();
1484
- var securityPatterns = /\b(auth|security|crypto|secret|credential|token|password|\.env|permission|acl|rbac|jwt|oauth|session|cookie)\b/;
1485
- if (securityPatterns.test(editFile) || editFile.includes('/security/') || editFile.includes('/auth/')) {
1486
- console.log('[SECURITY_EDIT] Security-sensitive file modified: ' + path.basename(editFile));
1487
- console.log('[SECURITY_EDIT] INSTRUCTION: Consider running a security review. Invoke Skill("code-review:code-review") with security focus, or run: npx monomind security scan --path "' + editFile + '"');
1488
- }
1489
- } catch (e) { /* non-fatal */ }
1490
-
1491
- // ── Smart Test/Build Suggestions (PE-001) ───────────────────────────
1492
- try {
1493
- var editFile = (hookInput.file_path || toolInput.file_path
1494
- || process.env.TOOL_INPUT_file_path || args[0] || '');
1495
- var editBase = path.basename(editFile).toLowerCase();
1496
- var editDir = path.dirname(editFile);
1497
- if (/\.(test|spec)\.(ts|js|tsx|jsx)$/.test(editBase)) {
1498
- console.log('[AUTO_SUGGEST] Test file modified — run: npm test -- --testPathPattern="' + path.basename(editFile) + '"');
1499
- } else if (editBase === 'package.json') {
1500
- console.log('[AUTO_SUGGEST] package.json changed — consider running: npm install');
1501
- } else if (editBase === 'tsconfig.json' || editBase === 'tsconfig.base.json') {
1502
- console.log('[AUTO_SUGGEST] TypeScript config changed — consider running: npm run build');
1503
- }
1504
- } catch (e) { /* non-fatal */ }
1505
-
1506
- // ── Monograph Incremental Rebuild ─────────────────────────────────────
1507
- // After every code file edit, trigger a background monograph rebuild so
1508
- // the knowledge graph stays current. Debounced via a lock file (5s cooldown).
1509
- try {
1510
- var editedFile = (hookInput.file_path || toolInput.file_path
1511
- || process.env.TOOL_INPUT_file_path || args[0] || '');
1512
- var codeExts = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|cs|cpp|c|rb|swift|php)$/i;
1513
- if (editedFile && codeExts.test(editedFile)) {
1514
- var lockFile = path.join(CWD, '.monomind', 'graph', '.rebuild-lock');
1515
- var now = Date.now();
1516
- var lastBuild = 0;
1517
- try { lastBuild = parseInt(fs.readFileSync(lockFile, 'utf-8').trim(), 10) || 0; } catch (e) {}
1518
- var COOLDOWN_MS = 5000; // 5-second debounce
1519
- if (now - lastBuild > COOLDOWN_MS) {
1520
- fs.writeFileSync(lockFile, String(now), 'utf-8');
1521
- var { spawn: spawnRebuild } = require('child_process');
1522
- var rebuildScript = "import { buildAsync } from '@monoes/monograph'; await buildAsync(" + JSON.stringify(CWD) + ");";
1523
- var graphDir = path.join(CWD, '.monomind', 'graph');
1524
- var logPath = path.join(graphDir, 'build.log');
1525
- var logFd;
1526
- try { logFd = fs.openSync(logPath, 'a'); } catch(e) { logFd = 'ignore'; }
1527
- var child = spawnRebuild(process.execPath, ['--input-type=module', '--eval', rebuildScript], {
1528
- detached: true, stdio: ['ignore', logFd, logFd], cwd: CWD,
1529
- });
1530
- child.unref();
1531
- console.log('[MONOGRAPH] Incremental rebuild triggered for ' + path.basename(editedFile));
1532
-
1533
- // Option C: fire ua-enrich.mjs in background after monograph rebuild
1534
- var uaEnrichScript = path.join(CWD, 'scripts', 'ua-enrich.mjs');
1535
- if (fs.existsSync(uaEnrichScript)) {
1536
- var uaChild = spawnRebuild(process.execPath, [uaEnrichScript, '--dir', CWD, '--file', editedFile, '--db', path.join(CWD, '.monomind', 'monograph.db')], {
1537
- detached: true, stdio: 'ignore', cwd: CWD,
1538
- });
1539
- uaChild.unref();
1540
- }
1541
- }
1542
- // Show importers of the edited file so Claude sees blast radius
1543
- try {
1544
- var mgDbPath4 = path.join(CWD, '.monomind', 'monograph.db');
1545
- if (fs.existsSync(mgDbPath4)) {
1546
- var mgMod4 = null;
1547
- mgMod4 = _requireMonograph();
1548
- if (mgMod4 && mgMod4.openDb) {
1549
- var db4 = mgMod4.openDb(mgDbPath4);
1550
- try {
1551
- var editedBase4 = path.basename(editedFile).replace(/\.[^.]+$/, '');
1552
- var editNode4 = db4.prepare("SELECT id, name, label FROM nodes WHERE file_path LIKE ? OR name = ? LIMIT 1")
1553
- .get('%' + path.sep + path.basename(editedFile), editedBase4);
1554
- if (editNode4) {
1555
- var editImporters4 = db4.prepare(
1556
- 'SELECT n2.name FROM edges e JOIN nodes n2 ON n2.id = e.source_id WHERE e.target_id = ? LIMIT 8'
1557
- ).all(editNode4.id);
1558
- if (editImporters4.length > 0) {
1559
- console.log('[MONOGRAPH_IMPACT] ' + editNode4.name + ' (' + editNode4.label + ') is depended on by: ' +
1560
- editImporters4.map(function(i) { return i.name; }).join(', '));
1561
- }
1562
- }
1563
- } finally { if (mgMod4.closeDb) mgMod4.closeDb(db4); }
1564
- }
1565
- }
1566
- } catch(e) { /* non-fatal */ }
1567
- }
1568
- } catch (e) { /* non-fatal */ }
1569
-
1570
- console.log('[OK] Edit recorded');
221
+ const h = require('./handlers/edit-handler.cjs');
222
+ await h.handle(hCtx);
1571
223
  },
1572
224
 
1573
- 'session-restore': async () => {
1574
- try {
1575
- if (session) {
1576
- var existing = session.restore && session.restore();
1577
- if (!existing) {
1578
- session.start && session.start();
1579
- }
1580
- } else {
1581
- console.log('[OK] Session restored: session-' + Date.now());
1582
- }
1583
- } catch (e) { console.log('[WARN] Session restore failed: ' + e.message); }
1584
-
1585
- // Stale helper detection — compare local helper hashes against the
1586
- // bundled (npm-installed) monomind copy and warn if they drift. This
1587
- // is what alerts the user that 'monomind init upgrade' would pick up
1588
- // new features (graph fallback routing, telemetry counters, etc).
1589
- try {
1590
- var crypto = require('crypto');
1591
- // Walk up from this file to find the monomind package root (looks for
1592
- // package.json with name === 'monomind' or '@monomind/cli').
1593
- function _findBundledHelpers() {
1594
- var helperPaths = [
1595
- path.join(__dirname),
1596
- path.join(CWD, 'node_modules', 'monomind', '.claude', 'helpers'),
1597
- path.join(CWD, 'node_modules', '@monoes', 'monomindcli', '.claude', 'helpers'),
1598
- ];
1599
- try {
1600
- var globalRoot = require('child_process')
1601
- .execSync('npm root -g 2>/dev/null', { encoding: 'utf-8', timeout: 2000 })
1602
- .trim();
1603
- if (globalRoot) {
1604
- helperPaths.push(path.join(globalRoot, 'monomind', '.claude', 'helpers'));
1605
- helperPaths.push(path.join(globalRoot, '@monoes', 'monomindcli', '.claude', 'helpers'));
1606
- }
1607
- } catch (_) {}
1608
- for (var i = 0; i < helperPaths.length; i++) {
1609
- if (fs.existsSync(path.join(helperPaths[i], 'hook-handler.cjs')) &&
1610
- helperPaths[i] !== path.join(CWD, '.claude', 'helpers')) {
1611
- return helperPaths[i];
1612
- }
1613
- }
1614
- return null;
1615
- }
1616
-
1617
- var bundledDir = _findBundledHelpers();
1618
- if (bundledDir) {
1619
- var helpersToCheck = ['hook-handler.cjs', 'statusline.cjs'];
1620
- var stale = [];
1621
- for (var hi = 0; hi < helpersToCheck.length; hi++) {
1622
- var hName = helpersToCheck[hi];
1623
- var localF = path.join(CWD, '.claude', 'helpers', hName);
1624
- var bundledF = path.join(bundledDir, hName);
1625
- if (!fs.existsSync(localF) || !fs.existsSync(bundledF)) continue;
1626
- try {
1627
- var hashL = crypto.createHash('sha256').update(fs.readFileSync(localF)).digest('hex');
1628
- var hashB = crypto.createHash('sha256').update(fs.readFileSync(bundledF)).digest('hex');
1629
- if (hashL !== hashB) stale.push(hName);
1630
- } catch (_) {}
1631
- }
1632
- if (stale.length > 0) {
1633
- console.log('[STALE_HELPERS] Project helpers differ from bundled version: ' + stale.join(', '));
1634
- console.log(' Run `npx monomind@latest init upgrade` to refresh and pick up the latest features.');
1635
- }
1636
- }
1637
- } catch (e) { /* non-fatal */ }
1638
- // Initialize intelligence (with timeout — #1530)
1639
- // Respects monomind.neural.enabled kill switch from settings.json
1640
- var neuralEnabled = true;
1641
- try {
1642
- var settingsPath = path.join(CWD, '.claude', 'settings.json');
1643
- if (fs.existsSync(settingsPath)) {
1644
- var settingsData = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
1645
- if (settingsData.monomind && settingsData.monomind.neural && settingsData.monomind.neural.enabled === false) {
1646
- neuralEnabled = false;
1647
- console.log('[NEURAL] Disabled via monomind.neural.enabled=false');
1648
- }
1649
- }
1650
- } catch (e) { /* non-fatal */ }
1651
- if (neuralEnabled && intelligence && intelligence.init) {
1652
- var initResult = await runWithTimeout(function() { return intelligence.init(); }, 'intelligence.init()');
1653
- if (initResult && initResult.nodes > 0) {
1654
- console.log('[INTELLIGENCE] Loaded ' + initResult.nodes + ' patterns, ' + initResult.edges + ' edges');
1655
- }
1656
- }
1657
- // GAP-001: Bridge hook-handler.cjs to @monomind/hooks compiled packages.
1658
- // Dynamic import() resolves ESM packages even from CJS — failures are silent.
1659
- try {
1660
- var hooksModule = await import('@monomind/hooks');
1661
- if (hooksModule && hooksModule.initDefaultWorkers) {
1662
- await runWithTimeout(function() { return hooksModule.initDefaultWorkers(); }, '@monomind/hooks.initDefaultWorkers()');
1663
- // Store reference so pre-task / post-task can call executeHooks (Tasks 26, 39)
1664
- _hooksModule = hooksModule;
1665
- console.log('[INFO] @monomind/hooks workers initialized');
1666
- }
1667
- } catch (e) { /* @monomind/hooks not compiled yet — skip */ }
1668
-
1669
- // ── Context Persistence Auto-Restore ───────────────────────────────────
1670
- // Restore archived conversation context from previous sessions
1671
- try {
1672
- var cpHook = await import('file://' + path.join(__dirname, 'context-persistence-hook.mjs'));
1673
- var restoreFn = (cpHook && cpHook.restore) || (cpHook && cpHook.default && cpHook.default.restore);
1674
- if (restoreFn) {
1675
- var restored = await runWithTimeout(function() { return restoreFn(); }, 'context-persistence.restore()');
1676
- if (restored && restored.turns > 0) {
1677
- console.log('[CONTEXT_RESTORED] ' + restored.turns + ' turns from previous session');
1678
- }
1679
- }
1680
- } catch (e) { /* non-fatal — context-persistence may not be available */ }
1681
-
1682
- // Task 28: AgentKnowledgeBase — preload shared knowledge context on session restore.
1683
- // Self-contained: auto-indexes project docs into chunks.jsonl, then keyword-searches
1684
- // them. Works without @monomind/memory being compiled. Falls back to KnowledgeRetriever
1685
- // if the compiled package IS available (richer dedup + formatting).
1686
- try {
1687
- var knowledgeDir = path.join(CWD, '.monomind', 'knowledge');
1688
- var indexed = _autoIndexKnowledge(knowledgeDir);
1689
- if (indexed > 0) {
1690
- console.log('[KNOWLEDGE_INDEXED] ' + indexed + ' chunks written from project sources');
1691
- }
1692
-
1693
- var kSearchFn = _buildKnowledgeSearchFn(knowledgeDir);
1694
- var sessionCtx = (hookInput && (hookInput.sessionId || hookInput.session_id))
1695
- ? 'session context: ' + (hookInput.sessionId || hookInput.session_id)
1696
- : 'project context general';
1697
-
1698
- // Prefer compiled KnowledgeRetriever for dedup + formatting; inline fallback otherwise
1699
- var memoryMod = null;
1700
- try { memoryMod = await import('@monomind/memory'); } catch (e) {}
1701
-
1702
- if (memoryMod && memoryMod.KnowledgeStore && memoryMod.KnowledgeRetriever) {
1703
- var kStore = new memoryMod.KnowledgeStore(knowledgeDir);
1704
- var kRetriever = new memoryMod.KnowledgeRetriever(kSearchFn, kStore);
1705
- var kResult = await kRetriever.retrieveForTask('shared', sessionCtx, 5);
1706
- if (kResult.excerpts.length > 0) {
1707
- console.log('[KNOWLEDGE_PRELOADED] ' + kResult.excerpts.length + ' excerpts (KnowledgeRetriever)');
1708
- }
1709
- } else {
1710
- // Inline fallback — no compiled deps needed
1711
- var directResults = await kSearchFn(sessionCtx, { namespace: 'knowledge:shared', limit: 5, minScore: 0.3 });
1712
- if (directResults.length > 0) {
1713
- console.log('[KNOWLEDGE_PRELOADED] ' + directResults.length + ' excerpts (direct keyword search)');
1714
- }
1715
- }
1716
- } catch (e) { /* non-fatal */ }
1717
-
1718
- // ── Monograph Context Injection ──────────────────────────────────────────
1719
- // On session start, query monograph DB for god nodes, inject as context,
1720
- // and write them into knowledge/chunks.jsonl for semantic search recall.
1721
- try {
1722
- var mgDbPath = path.join(CWD, '.monomind', 'monograph.db');
1723
- if (fs.existsSync(mgDbPath)) {
1724
- var mgDb = _openMonographDb();
1725
- if (mgDb) {
1726
- try {
1727
- var mgNodeCount = mgDb.prepare('SELECT COUNT(*) AS c FROM nodes').get().c;
1728
- var mgEdgeCount = mgDb.prepare('SELECT COUNT(*) AS c FROM edges').get().c;
1729
- var mgGodNodes = mgDb.prepare(
1730
- "SELECT n.name, n.label, n.file_path, " +
1731
- "(SELECT COUNT(*) FROM edges WHERE source_id=n.id OR target_id=n.id) AS deg " +
1732
- "FROM nodes n " +
1733
- "WHERE n.label NOT IN ('Concept') " +
1734
- "AND n.file_path IS NOT NULL AND n.file_path != '' " +
1735
- "AND n.file_path NOT LIKE '%/node_modules/%' AND n.file_path NOT LIKE '%node_modules%' " +
1736
- "ORDER BY deg DESC LIMIT 12"
1737
- ).all();
1738
-
1739
- // Check graph staleness: compare stored commit hash with current HEAD.
1740
- var mgStaleIndicator = '';
1741
- try {
1742
- var mgLastCommitRow = null;
1743
- try { mgLastCommitRow = mgDb.prepare("SELECT value FROM index_meta WHERE key='ua_last_commit'").get(); } catch (_) {}
1744
- if (mgLastCommitRow && mgLastCommitRow.value) {
1745
- var { execFileSync: mgExec } = require('child_process');
1746
- var currentHead = '';
1747
- try { currentHead = mgExec('git', ['rev-parse', 'HEAD'], { cwd: CWD, encoding: 'utf-8' }).trim(); } catch (_) {}
1748
- if (currentHead && currentHead !== mgLastCommitRow.value) {
1749
- var commitsBehind = 0;
1750
- try {
1751
- var revList = mgExec('git', ['rev-list', '--count', mgLastCommitRow.value + '..' + currentHead], { cwd: CWD, encoding: 'utf-8' }).trim();
1752
- commitsBehind = parseInt(revList, 10) || 0;
1753
- } catch (_) {}
1754
- if (commitsBehind > 0) {
1755
- mgStaleIndicator = ' [⚡ graph ' + commitsBehind + ' commit' + (commitsBehind === 1 ? '' : 's') + ' behind — run: npx monomind monograph build]';
1756
- }
1757
- }
1758
- }
1759
- } catch (_) {}
1760
-
1761
- if (mgGodNodes.length > 0) {
1762
- var mgGodStr = mgGodNodes.slice(0, 8).map(function(n) {
1763
- return n.name + ' (' + n.label + ', ' + n.deg + ' links)';
1764
- }).join(', ');
1765
- console.log('[MONOGRAPH_CONTEXT] ' + mgNodeCount + ' nodes · ' + mgEdgeCount + ' edges. Key nodes: ' + mgGodStr + mgStaleIndicator);
1766
- // Write god nodes into knowledge chunks so semantic search finds them
1767
- var mgKnowledgeDir = path.join(CWD, '.monomind', 'knowledge');
1768
- var mgChunksFile = path.join(mgKnowledgeDir, 'chunks.jsonl');
1769
- try {
1770
- fs.mkdirSync(mgKnowledgeDir, { recursive: true });
1771
- var mgGodChunk = JSON.stringify({
1772
- id: 'monograph-god-nodes',
1773
- text: 'Codebase architecture — high-centrality nodes (most depended-on): ' + mgGodNodes.map(function(n) {
1774
- return n.name + ' [' + n.label + '] at ' + (n.file_path || '') + ' (' + n.deg + ' connections)';
1775
- }).join('; '),
1776
- namespace: 'knowledge:monograph',
1777
- metadata: { label: 'monograph-god-nodes', nodes: mgNodeCount, edges: mgEdgeCount }
1778
- });
1779
- var mgExisting = [];
1780
- try { mgExisting = fs.readFileSync(mgChunksFile, 'utf-8').trim().split('\n').filter(Boolean); } catch(e) {}
1781
- mgExisting = mgExisting.filter(function(line) {
1782
- try { return JSON.parse(line).id !== 'monograph-god-nodes'; } catch(e) { return true; }
1783
- });
1784
- mgExisting.push(mgGodChunk);
1785
- fs.writeFileSync(mgChunksFile, mgExisting.join('\n') + '\n');
1786
- } catch(e) {}
1787
- }
1788
- } catch(e) { /* non-fatal */ }
1789
- }
1790
- }
1791
- } catch(e) { /* non-fatal */ }
1792
-
1793
- // Task 23: SharedInstructions — auto-load .agents/shared_instructions.md on session restore
1794
- // Hard limit: 1500 chars (~375 tokens). Content beyond this is truncated and flagged.
1795
- var SI_CHAR_LIMIT = 1500;
1796
- var applySharedInstrLimit = function(content, source) {
1797
- if (content.length > SI_CHAR_LIMIT) {
1798
- console.warn('[SHARED_INSTRUCTIONS_OVERLIMIT] ' + content.length + ' chars exceeds limit of ' + SI_CHAR_LIMIT +
1799
- ' — truncating. Edit ' + source + ' to stay under limit.');
1800
- return content.slice(0, SI_CHAR_LIMIT) + '\n… [truncated — file exceeds ' + SI_CHAR_LIMIT + ' char limit]';
1801
- }
1802
- return content;
1803
- };
1804
- try {
1805
- var siMod = await import('file://' + path.join(CWD, 'packages/@monomind/cli/dist/src/agents/shared-instructions-loader.js'));
1806
- var loader = siMod.sharedInstructionsLoader || (siMod.SharedInstructionsLoader ? new siMod.SharedInstructionsLoader() : null);
1807
- if (loader) {
1808
- var sharedInstr = loader.getSharedInstructions(CWD);
1809
- if (sharedInstr) {
1810
- var sharedInstrSafe = applySharedInstrLimit(sharedInstr, '.agents/shared_instructions.md');
1811
- console.log('[SHARED_INSTRUCTIONS] Loaded ' + sharedInstrSafe.length + ' chars from .agents/shared_instructions.md');
1812
- console.log(sharedInstrSafe);
1813
- }
1814
- }
1815
- } catch (e) {
1816
- // Try direct filesystem fallback
1817
- try {
1818
- var siPath = path.join(CWD, '.agents', 'shared_instructions.md');
1819
- if (fs.existsSync(siPath)) {
1820
- var siContent = fs.readFileSync(siPath, 'utf-8');
1821
- var siContentSafe = applySharedInstrLimit(siContent, siPath);
1822
- console.log('[SHARED_INSTRUCTIONS] Loaded ' + siContentSafe.length + ' chars from .agents/shared_instructions.md');
1823
- console.log(siContentSafe);
1824
- }
1825
- } catch (e2) { /* non-fatal */ }
1826
- }
1827
-
1828
- // Memory Palace — inject L0 (identity) + L1 (essential story) into session context
1829
- try {
1830
- var palace = require('./memory-palace.cjs');
1831
- var palaceContext = palace.wakeUp(CWD);
1832
- if (palaceContext) {
1833
- console.log(palaceContext);
1834
- }
1835
- } catch (e) { /* non-fatal — palace not available */ }
1836
-
1837
- // ── Periodic Update Check (once per day) ──────────────────────────────
1838
- try {
1839
- var updateCheckFile = path.join(CWD, '.monomind', 'last-update-check.json');
1840
- var shouldCheck = true;
1841
- if (fs.existsSync(updateCheckFile)) {
1842
- var lastCheck = JSON.parse(fs.readFileSync(updateCheckFile, 'utf-8'));
1843
- var hoursSince = (Date.now() - new Date(lastCheck.timestamp).getTime()) / (1000 * 60 * 60);
1844
- if (hoursSince < 24) shouldCheck = false;
1845
- }
1846
- if (shouldCheck) {
1847
- // Non-blocking: write marker immediately, check asynchronously
1848
- fs.mkdirSync(path.join(CWD, '.monomind'), { recursive: true });
1849
- fs.writeFileSync(updateCheckFile, JSON.stringify({ timestamp: new Date().toISOString() }), 'utf-8');
1850
- try {
1851
- var localPkg = path.join(CWD, 'packages/@monomind/cli/package.json');
1852
- if (fs.existsSync(localPkg)) {
1853
- var localVer = JSON.parse(fs.readFileSync(localPkg, 'utf-8')).version;
1854
- if (localVer) {
1855
- // Non-blocking spawn — never holds the event loop during hook execution
1856
- var spawnFn = require('child_process').spawn;
1857
- var child = spawnFn('npm', ['view', '@monomind/cli', 'version'], {
1858
- stdio: ['ignore', 'pipe', 'ignore'],
1859
- shell: false,
1860
- });
1861
- // Required: without an error listener, ENOENT (npm not in PATH) crashes the process
1862
- child.on('error', function() {});
1863
- var out = '';
1864
- child.stdout.on('data', function(d) { out += d; });
1865
- child.on('close', function() {
1866
- var current = out.trim();
1867
- var pendingUpdatePath = path.join(CWD, '.monomind', 'pending-update.json');
1868
- if (current && current !== localVer) {
1869
- // Write result to a sidecar file — picked up on next session-start
1870
- try {
1871
- fs.writeFileSync(
1872
- pendingUpdatePath,
1873
- JSON.stringify({ from: localVer, to: current, checkedAt: new Date().toISOString() }),
1874
- 'utf-8'
1875
- );
1876
- } catch (e2) {}
1877
- } else if (current) {
1878
- // Versions match — clear any stale notification so it doesn't show forever
1879
- try { fs.unlinkSync(pendingUpdatePath); } catch (e2) {}
1880
- }
1881
- });
1882
- child.unref();
1883
- }
1884
- }
1885
- } catch (e) { /* npm not available — skip silently */ }
1886
- }
1887
- // Surface any previously-detected update on every session restore (not just on check day)
1888
- try {
1889
- var pendingUpdate = path.join(CWD, '.monomind', 'pending-update.json');
1890
- if (fs.existsSync(pendingUpdate)) {
1891
- var upd = JSON.parse(fs.readFileSync(pendingUpdate, 'utf-8'));
1892
- if (upd && upd.from && upd.to && upd.from !== upd.to) {
1893
- console.log('[UPDATE_AVAILABLE] @monomind/cli ' + upd.from + ' → ' + upd.to + ' (run: npx monomind update)');
1894
- }
1895
- }
1896
- } catch (e) {}
1897
- } catch (e) { /* non-fatal */ }
1898
-
1899
- // ── Daemon Auto-Start Check ────────────────────────────────────────────
1900
- // If daemon is not running, suggest starting it (or auto-start if config says so)
1901
- try {
1902
- var daemonPid = path.join(CWD, '.monomind', 'daemon.pid');
1903
- var daemonRunning = false;
1904
- if (fs.existsSync(daemonPid)) {
1905
- try {
1906
- var pid = parseInt(fs.readFileSync(daemonPid, 'utf-8').trim(), 10);
1907
- process.kill(pid, 0); // throws if process doesn't exist
1908
- daemonRunning = true;
1909
- } catch (e) { /* pid stale */ }
1910
- }
1911
- if (!daemonRunning) {
1912
- // Check config for autoStart preference
1913
- var daemonCfg = {};
1914
- try {
1915
- var cfgPath = path.join(CWD, 'monomind.config.json');
1916
- if (fs.existsSync(cfgPath)) daemonCfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8')).daemon || {};
1917
- } catch (e) {}
1918
- if (daemonCfg.autoStart) {
1919
- // Auto-start daemon in background
1920
- var spawn = require('child_process').spawn;
1921
- var child = spawn('npx', ['monomind', 'daemon', 'start'], {
1922
- cwd: CWD, detached: true, stdio: 'ignore'
1923
- });
1924
- child.on('error', function() {});
1925
- child.unref();
1926
- console.log('[DAEMON_AUTOSTART] Background daemon started (pid ' + child.pid + ')');
1927
- } else {
1928
- console.log('[DAEMON_STOPPED] Background daemon is not running. To auto-start, set daemon.autoStart=true in monomind.config.json or run: npx monomind daemon start');
1929
- }
1930
- }
1931
- } catch (e) { /* non-fatal */ }
1932
-
1933
- // Token Usage — inject daily/monthly cost summary from JSONL session logs
1934
- try {
1935
- var tokenTracker = require('./token-tracker.cjs');
1936
- var tokenSummary = tokenTracker.quickSummary();
1937
- if (tokenSummary) {
1938
- console.log(tokenSummary);
1939
- }
1940
- // Write structured cache for statusline (best-effort, non-blocking)
1941
- try {
1942
- var tokenData = tokenTracker.quickSummaryData();
1943
- if (tokenData) {
1944
- var metricsDir = path.join(CWD, '.monomind', 'metrics');
1945
- if (!fs.existsSync(metricsDir)) fs.mkdirSync(metricsDir, { recursive: true });
1946
- tokenData.cachedAt = new Date().toISOString();
1947
- fs.writeFileSync(path.join(metricsDir, 'token-summary.json'), JSON.stringify(tokenData), 'utf-8');
1948
- }
1949
- } catch (_) { /* ignore cache write failure */ }
1950
- } catch (e) { /* non-fatal — token tracker not available */ }
1951
225
 
1952
- // ── Registry Surfacing (SR-001) ─────────────────────────────────────
1953
- // Show agent registry summary so users know what's available
1954
- try {
1955
- var regPath = path.join(CWD, '.monomind', 'registry.json');
1956
- if (fs.existsSync(regPath)) {
1957
- var reg = JSON.parse(fs.readFileSync(regPath, 'utf-8'));
1958
- var agentCount = (reg.agents || []).length;
1959
- if (agentCount > 0) {
1960
- console.log('[REGISTRY] ' + agentCount + ' agents available in registry');
1961
- }
1962
- }
1963
- } catch (e) { /* non-fatal */ }
1964
-
1965
- // ── Monomind Control UI Status ────────────────────────────────────────
1966
- try {
1967
- var http = require('http');
1968
- var controlPort = 4242;
1969
- var req = http.get('http://localhost:' + controlPort + '/', function(res) {
1970
- if (res.statusCode === 200) {
1971
- console.log('[CONTROL_UI] UP — http://localhost:' + controlPort);
1972
- }
1973
- res.resume();
1974
- });
1975
- req.on('error', function() {
1976
- console.log('[CONTROL_UI] offline — run: npx monomind mcp start');
1977
- });
1978
- req.setTimeout(800, function() { req.destroy(); });
1979
- } catch (e) { /* non-fatal */ }
1980
-
1981
- // ── Worker Queue Resume (SR-003) ────────────────────────────────────
1982
- try {
1983
- var dispatchDir = path.join(CWD, '.monomind', 'worker-dispatch');
1984
- if (fs.existsSync(dispatchDir)) {
1985
- var pendingFiles = fs.readdirSync(dispatchDir).filter(function(f) { return f.startsWith('pending-'); });
1986
- if (pendingFiles.length > 0) {
1987
- console.log('[WORKER_RESUME] ' + pendingFiles.length + ' worker dispatch(es) pending from prior session');
1988
- }
1989
- }
1990
- } catch (e) { /* non-fatal */ }
226
+ 'session-restore': async () => {
227
+ const h = require('./handlers/session-restore-handler.cjs');
228
+ await h.handleRestore(hCtx);
1991
229
  },
1992
230
 
1993
- 'session-end': async () => {
1994
- // Consolidate intelligence (with timeout — #1530)
1995
- if (intelligence && intelligence.consolidate) {
1996
- var consResult = await runWithTimeout(function() { return intelligence.consolidate(); }, 'intelligence.consolidate()');
1997
- if (consResult && consResult.entries > 0) {
1998
- var msg = '[INTELLIGENCE] Consolidated: ' + consResult.entries + ' entries, ' + consResult.edges + ' edges';
1999
- if (consResult.newEntries > 0) msg += ', ' + consResult.newEntries + ' new';
2000
- msg += ', PageRank recomputed';
2001
- console.log(msg);
2002
- }
2003
- }
2004
- try {
2005
- if (session && session.end) {
2006
- session.end();
2007
- } else {
2008
- console.log('[OK] Session ended');
2009
- }
2010
- } catch (e) { console.log('[WARN] Session end failed: ' + e.message); }
2011
231
 
2012
- // ── Routing Feedback Loop (SE-001) ────────────────────────────────────
2013
- // Persist routing accuracy feedback so the router improves over sessions
2014
- try {
2015
- var feedbackPath = path.join(CWD, '.monomind', 'routing-feedback.jsonl');
2016
- var lastRoutePath = path.join(CWD, '.monomind', 'last-route.json');
2017
- if (fs.existsSync(lastRoutePath)) {
2018
- var lastRoute = JSON.parse(fs.readFileSync(lastRoutePath, 'utf-8'));
2019
- var feedbackEntry = {
2020
- timestamp: new Date().toISOString(),
2021
- suggestedAgent: lastRoute.agent,
2022
- confidence: lastRoute.confidence,
2023
- sessionId: hookInput.sessionId || hookInput.session_id || '',
2024
- // If intelligence gave feedback during session, it's recorded here
2025
- intelligenceFeedback: (intelligence && intelligence.getSessionStats) ? intelligence.getSessionStats() : null,
2026
- };
2027
- fs.appendFileSync(feedbackPath, JSON.stringify(feedbackEntry) + '\n', 'utf-8');
2028
- // Rotate: keep last 1000 lines to prevent unbounded growth
2029
- try {
2030
- var raw = fs.readFileSync(feedbackPath, 'utf-8');
2031
- var lines = raw.split('\n').filter(Boolean);
2032
- if (lines.length > 1000) {
2033
- fs.writeFileSync(feedbackPath, lines.slice(-1000).join('\n') + '\n', 'utf-8');
2034
- }
2035
- } catch (e2) { /* rotation is best-effort */ }
2036
- }
2037
- } catch (e) { /* non-fatal */ }
2038
-
2039
- // Memory Palace tombstone writes removed — redundant with raw session JSONL
2040
-
2041
- // ── Learning Service Auto-Consolidation ─────────────────────────────
2042
- // Consolidate learned patterns from short-term to long-term storage
2043
- try {
2044
- var learningService = await import('file://' + path.join(__dirname, 'learning-service.mjs'));
2045
- if (learningService && learningService.consolidate) {
2046
- var lResult = await runWithTimeout(function() { return learningService.consolidate(); }, 'learning.consolidate()');
2047
- if (lResult && lResult.promoted > 0) {
2048
- console.log('[LEARNING] Consolidated: ' + lResult.promoted + ' patterns promoted to long-term');
2049
- }
2050
- } else if (learningService && learningService.default && learningService.default.consolidate) {
2051
- var lResult2 = await runWithTimeout(function() { return learningService.default.consolidate(); }, 'learning.consolidate()');
2052
- if (lResult2 && lResult2.promoted > 0) {
2053
- console.log('[LEARNING] Consolidated: ' + lResult2.promoted + ' patterns promoted to long-term');
2054
- }
2055
- }
2056
- } catch (e) { /* non-fatal — learning-service may need better-sqlite3 */ }
2057
-
2058
- // ── Context Persistence Auto-Archive ─────────────────────────────────
2059
- // Archive conversation context so it survives compaction and new sessions
2060
- try {
2061
- var cpHook = await import('file://' + path.join(__dirname, 'context-persistence-hook.mjs'));
2062
- if (cpHook && cpHook.archive) {
2063
- await runWithTimeout(function() { return cpHook.archive(); }, 'context-persistence.archive()');
2064
- console.log('[CONTEXT_PERSIST] Session transcript archived');
2065
- } else if (cpHook && cpHook.default && cpHook.default.archive) {
2066
- await runWithTimeout(function() { return cpHook.default.archive(); }, 'context-persistence.archive()');
2067
- console.log('[CONTEXT_PERSIST] Session transcript archived');
2068
- }
2069
- } catch (e) { /* non-fatal — context-persistence may not export archive() */ }
2070
-
2071
- // ── Worker Queue Cleanup ─────────────────────────────────────────────
2072
- // Process and clean up any pending worker dispatch files
2073
- try {
2074
- var dispatchDir = path.join(CWD, '.monomind', 'worker-dispatch');
2075
- if (fs.existsSync(dispatchDir)) {
2076
- var pending = fs.readdirSync(dispatchDir).filter(function(f) { return f.startsWith('pending-'); });
2077
- if (pending.length > 0) {
2078
- console.log('[WORKER_CLEANUP] ' + pending.length + ' worker dispatch(es) pending from this session');
2079
- }
2080
- // Move to processed
2081
- var processedDir = path.join(dispatchDir, 'processed');
2082
- fs.mkdirSync(processedDir, { recursive: true });
2083
- pending.forEach(function(f) {
2084
- try {
2085
- fs.renameSync(path.join(dispatchDir, f), path.join(processedDir, f));
2086
- } catch (e) { /* ignore */ }
2087
- });
2088
- // Trim processed/ to last 200 files to prevent unbounded growth
2089
- try {
2090
- var processedFiles = fs.readdirSync(processedDir)
2091
- .filter(function(f) { return f.startsWith('pending-'); })
2092
- .map(function(f) { var fp = path.join(processedDir, f); var mt = 0; try { mt = fs.statSync(fp).mtimeMs; } catch(e2){} return { f: f, mt: mt }; })
2093
- .sort(function(a, b) { return a.mt - b.mt; });
2094
- if (processedFiles.length > 200) {
2095
- processedFiles.slice(0, processedFiles.length - 200).forEach(function(item) {
2096
- try { fs.unlinkSync(path.join(processedDir, item.f)); } catch (e2) { /* ignore */ }
2097
- });
2098
- }
2099
- } catch (e2) { /* non-fatal */ }
2100
- }
2101
- } catch (e) { /* non-fatal */ }
232
+ 'session-end': async () => {
233
+ const h = require('./handlers/session-handler.cjs');
234
+ await h.handleEnd(hCtx);
2102
235
  },
2103
236
 
2104
- 'pre-task': async () => {
2105
- if (session && session.metric) {
2106
- try { session.metric('tasks'); } catch (e) { /* no active session */ }
2107
- }
2108
-
2109
- // ── Task 27: PerRunModelTier — inline complexity scoring ───────────────
2110
- var taskStr = typeof prompt === 'string' ? prompt : '';
2111
- if (taskStr) {
2112
- var score = 50;
2113
- var lower = taskStr.toLowerCase();
2114
- var words = taskStr.trim().split(/\s+/).length;
2115
- if (words < 20) score -= 20;
2116
- if (words > 100) score += 20;
2117
- if (words > 200) score += 10;
2118
- var highKw = ['architecture','distributed','security audit','cve','consensus','fault-tolerant','migrate','refactor across','orchestrat','design system','database schema','performance optim','threat model','encryption','zero-knowledge'];
2119
- var lowKwRe = /\b(format|list|rename|sort|typo|lint|log|comment|print|echo|delete unused|remove import)\b/i;
2120
- if (highKw.some(function(k) { return lower.includes(k); })) score += 10;
2121
- if (lowKwRe.test(lower)) score -= 10;
2122
- if (/(?:step\s*\d|first[\s,].*then[\s,]|phase\s*\d)/i.test(taskStr)) score += 10;
2123
- if (/```[\s\S]*?```/.test(taskStr) || /\b[\w.-]+\/[\w./-]+\b/.test(taskStr)) score += 5;
2124
- score = Math.max(0, Math.min(100, score));
2125
- var tier = score < 30 ? 'haiku' : score > 70 ? 'opus' : 'sonnet';
2126
- console.log('[TASK_MODEL_RECOMMENDATION] Use model="' + tier + '" (complexity=' + score + ')');
2127
- }
2128
- // Task 06: AutoRetry — signal retry policy only if coordinator path is active
2129
- if (hookInput.swarmCoordinator || hookInput.coordinator || hookInput.useRetry) {
2130
- console.log('[AUTO_RETRY_ENABLED] maxAttempts=3 strategy=exponential-backoff backoffMs=1000');
2131
- }
2132
237
 
2133
- if (router && prompt) {
2134
- var routeFn = router.routeTaskSemantic || router.routeTask;
2135
- var result = await Promise.resolve(routeFn(prompt));
2136
- console.log('[INFO] Task routed to: ' + result.agent + ' (confidence: ' + result.confidence + ')');
2137
- } else {
2138
- console.log('[OK] Task started');
2139
- }
2140
-
2141
- // Task 24: PromptVersioning — resolve prompt variant before agent spawn
2142
- try {
2143
- var memMod = await import('@monomind/memory');
2144
- if (memMod && memMod.PromptVersionStore) {
2145
- var pvStore = new memMod.PromptVersionStore(path.join(CWD, '.monomind', 'prompt-versions'));
2146
- var pvMod = await import('file://' + path.join(CWD, 'packages/@monomind/cli/dist/src/agents/prompt-experiment.js'));
2147
- if (pvMod && pvMod.PromptExperimentRouter) {
2148
- var pvRouter = new pvMod.PromptExperimentRouter(pvStore);
2149
- var agentSlug24 = hookInput.agentSlug || hookInput.agentType || hookInput.agent_type || 'unknown';
2150
- if (agentSlug24 !== 'unknown') {
2151
- var resolved = pvRouter.resolvePromptForSpawn(agentSlug24);
2152
- if (resolved.version) {
2153
- console.log('[PROMPT_VERSION] ' + agentSlug24 + ' v' + resolved.version + (resolved.isCandidate ? ' (experiment candidate)' : ''));
2154
- }
2155
- }
2156
- }
2157
- }
2158
- } catch (e) { /* not available or no experiment */ }
2159
-
2160
- // Monograph impact — detect changed files and surface their dependents
2161
- try {
2162
- var mgDbPath1 = path.join(CWD, '.monomind', 'monograph.db');
2163
- if (fs.existsSync(mgDbPath1)) {
2164
- var execSync1 = require('child_process').execSync;
2165
- var changedFiles1 = '';
2166
- try { changedFiles1 = execSync1('git diff --name-only HEAD 2>/dev/null || git diff --name-only 2>/dev/null', { cwd: CWD, timeout: 3000, shell: true }).toString().trim(); } catch(e) {}
2167
- if (changedFiles1) {
2168
- var mgMod1 = null;
2169
- mgMod1 = _requireMonograph();
2170
- if (mgMod1 && mgMod1.openDb) {
2171
- var db1 = mgMod1.openDb(mgDbPath1);
2172
- try {
2173
- var fileList1 = changedFiles1.split('\n').filter(Boolean).slice(0, 8);
2174
- var impacted1 = [];
2175
- for (var fi = 0; fi < fileList1.length; fi++) {
2176
- var fBase = path.basename(fileList1[fi]);
2177
- var fNode = db1.prepare("SELECT id, name, label FROM nodes WHERE file_path LIKE ? LIMIT 1").get('%' + fBase);
2178
- if (fNode) {
2179
- var fImporters = db1.prepare(
2180
- 'SELECT n2.name FROM edges e JOIN nodes n2 ON n2.id = e.source_id WHERE e.target_id = ? LIMIT 5'
2181
- ).all(fNode.id);
2182
- var entry = fNode.name + ' (' + fNode.label + ')';
2183
- if (fImporters.length) entry += ' ← ' + fImporters.map(function(i){ return i.name; }).join(', ');
2184
- impacted1.push(entry);
2185
- }
2186
- }
2187
- if (impacted1.length > 0) {
2188
- console.log('[MONOGRAPH_IMPACT] Changed files and their dependents: ' + impacted1.join(' | '));
2189
- }
2190
-
2191
- // Effective blast radius — second pass using first impacted node
2192
- try {
2193
- if (fileList1.length > 0 && mgMod1.effectiveBlastRadius) {
2194
- var firstFile1 = path.basename(fileList1[0]);
2195
- var firstNode1 = db1.prepare("SELECT id, name, label FROM nodes WHERE file_path LIKE ? LIMIT 1").get('%' + firstFile1);
2196
- if (firstNode1) {
2197
- var blastResults1 = mgMod1.effectiveBlastRadius(db1, firstNode1.id, { maxDepth: 4 });
2198
- if (blastResults1 && blastResults1.length > 0) {
2199
- var bwdCount1 = blastResults1.filter(function(r){ return r.direction === 'backward' || r.direction === 'both'; }).length;
2200
- var fwdCount1 = blastResults1.filter(function(r){ return r.direction === 'forward' || r.direction === 'both'; }).length;
2201
- var topBlast1 = blastResults1.slice(0, 8).map(function(r){
2202
- return '[' + r.direction + ':' + r.hops + '] ' + r.nodeName + ' (' + r.nodeLabel + ')';
2203
- });
2204
- console.log('[MONOGRAPH_BLAST_RADIUS] Node: ' + firstNode1.name + ' | forward=' + fwdCount1 + ' backward=' + bwdCount1 + ' | ' + topBlast1.join(', '));
2205
- }
2206
- }
2207
- }
2208
- } catch(blastErr) { /* non-fatal */ }
2209
- } finally { if (mgMod1.closeDb) mgMod1.closeDb(db1); }
2210
- }
2211
- }
2212
- }
2213
- } catch(e) { /* non-fatal */ }
2214
-
2215
- // Bridge to @monomind/hooks registry — fires Tasks 26 (PromptAssembler) and any other PreTask hooks
2216
- if (_hooksModule && _hooksModule.executeHooks && _hooksModule.HookEvent) {
2217
- try {
2218
- await _hooksModule.executeHooks(_hooksModule.HookEvent.PreTask, {
2219
- task: typeof prompt === 'string' ? { description: prompt, id: hookInput.taskId || '' } : null,
2220
- sessionId: hookInput.sessionId || hookInput.session_id || 'default',
2221
- }, { continueOnError: true, timeout: 2000 });
2222
- } catch (e) { /* non-fatal */ }
2223
- }
238
+ 'pre-task': async () => {
239
+ const h = require('./handlers/task-handler.cjs');
240
+ await h.handlePreTask(hCtx);
2224
241
  },
2225
242
 
2226
- 'post-task': async () => {
2227
- var taskSuccess = hookInput.success !== false && hookInput.status !== 'failed';
2228
- if (intelligence && intelligence.feedback) {
2229
- try {
2230
- intelligence.feedback(true);
2231
- } catch (e) { /* non-fatal */ }
2232
- }
2233
- // Each TeammateIdle/TaskCompleted = one agent done → remove oldest registration (FIFO)
2234
- const regDir = path.join(CWD, '.monomind', 'agents', 'registrations');
2235
- try {
2236
- if (fs.existsSync(regDir)) {
2237
- const files = fs.readdirSync(regDir).filter(f => f.endsWith('.json'));
2238
- if (files.length > 0) {
2239
- // Sort by mtime ascending (oldest first) and remove the oldest one
2240
- const sorted = files
2241
- .map(f => ({ f, mtime: (() => { try { return fs.statSync(path.join(regDir, f)).mtimeMs; } catch { return 0; } })() }))
2242
- .sort((a, b) => a.mtime - b.mtime);
2243
- try { fs.unlinkSync(path.join(regDir, sorted[0].f)); } catch { /* ignore */ }
2244
- }
2245
- // Also purge any stragglers older than 30 min
2246
- const now = Date.now();
2247
- for (const f of fs.readdirSync(regDir).filter(f => f.endsWith('.json'))) {
2248
- try { if (now - fs.statSync(path.join(regDir, f)).mtimeMs > 30 * 60 * 1000) fs.unlinkSync(path.join(regDir, f)); } catch { /* ignore */ }
2249
- }
2250
- const remaining = fs.readdirSync(regDir).filter(f => f.endsWith('.json')).length;
2251
- const _actPath = path.join(CWD, '.monomind', 'metrics', 'swarm-activity.json');
2252
- let _prevLastActive = 0;
2253
- try { _prevLastActive = (JSON.parse(fs.readFileSync(_actPath, 'utf-8'))?.swarm?.lastActive) || 0; } catch { /* ignore */ }
2254
- fs.writeFileSync(_actPath, JSON.stringify({
2255
- timestamp: new Date().toISOString(),
2256
- swarm: {
2257
- active: remaining > 0,
2258
- agent_count: remaining,
2259
- coordination_active: remaining > 0,
2260
- lastActive: Math.max(remaining, _prevLastActive), // preserve peak across completion
2261
- },
2262
- }));
2263
- }
2264
- } catch (e) { /* non-fatal */ }
2265
- // Bridge to @monomind/hooks registry — fires Tasks 39 (SpecializationScorer) and any other PostTask hooks
2266
- if (_hooksModule && _hooksModule.executeHooks && _hooksModule.HookEvent) {
2267
- try {
2268
- await _hooksModule.executeHooks(_hooksModule.HookEvent.PostTask, {
2269
- task: {
2270
- id: hookInput.taskId || hookInput.task_id || '',
2271
- status: taskSuccess ? 'completed' : 'failed',
2272
- agentSlug: hookInput.agentSlug || hookInput.agent_slug || 'unknown',
2273
- type: hookInput.taskType || hookInput.task_type || 'general',
2274
- },
2275
- success: taskSuccess,
2276
- latencyMs: hookInput.latencyMs || hookInput.latency_ms || 0,
2277
- qualityScore: hookInput.qualityScore || hookInput.quality_score,
2278
- }, { continueOnError: true, timeout: 2000 });
2279
- } catch (e) { /* non-fatal */ }
2280
- }
2281
- // Task 35: TerminationConditions — detect halted swarms via halt-signal
2282
- try {
2283
- var haltMod = await import('file://' + path.join(CWD, 'packages/@monomind/cli/dist/src/agents/halt-signal.js'));
2284
- if (haltMod && haltMod.isHalted) {
2285
- var swarmId35 = hookInput.swarmId || hookInput.swarm_id || 'default';
2286
- if (haltMod.isHalted(swarmId35)) {
2287
- console.warn('[HALT_DETECTED] Swarm ' + swarmId35 + ' has an active halt signal — agents should stop');
2288
- }
2289
- }
2290
- } catch (e) {
2291
- // Try direct file check
2292
- try {
2293
- var haltFile = path.join(CWD, 'data', 'halt-signals.jsonl');
2294
- if (fs.existsSync(haltFile)) {
2295
- var haltLines = fs.readFileSync(haltFile, 'utf-8').trim().split('\n').filter(Boolean);
2296
- if (haltLines.length > 0) {
2297
- console.warn('[HALT_DETECTED] ' + haltLines.length + ' halt signal(s) present');
2298
- }
2299
- }
2300
- } catch (e2) { /* non-fatal */ }
2301
- }
2302
-
2303
- // Task 37: DeadLetterQueue — enqueue failed tasks when retries exhausted
2304
- try {
2305
- if (!taskSuccess) {
2306
- var dlqMod = await import('file://' + path.join(CWD, 'packages/@monomind/cli/dist/src/dlq/dlq-writer.js'));
2307
- if (dlqMod && dlqMod.DLQWriter) {
2308
- var dlqDir = path.join(CWD, '.monomind', 'dlq');
2309
- var dlqWriter = new dlqMod.DLQWriter(dlqDir);
2310
- dlqWriter.enqueue({
2311
- toolName: 'post-task',
2312
- originalPayload: { taskId: hookInput.taskId || '', agentSlug: hookInput.agentSlug || 'unknown' },
2313
- deliveryAttempts: [{ attempt: 1, timestamp: new Date().toISOString(), error: hookInput.error || 'task failed' }],
2314
- agentId: hookInput.agentSlug || hookInput.agent_slug,
2315
- swarmId: hookInput.swarmId || hookInput.swarm_id,
2316
- });
2317
- console.log('[DLQ_ENQUEUED] Failed task ' + (hookInput.taskId || 'unknown') + ' sent to dead-letter queue');
2318
- }
2319
- }
2320
- } catch (e) { /* non-fatal */ }
2321
-
2322
- // Memory Palace task drawer writes removed — use auto-memory files for task context
2323
-
2324
- // ── Worker Auto-Dispatch ──────────────────────────────────────────────
2325
- // Auto-dispatch background workers based on task outcome
2326
- try {
2327
- var taskDesc = (typeof prompt === 'string' ? prompt : hookInput.description || '').toLowerCase();
2328
- var workersToDispatch = [];
2329
-
2330
- // Always consolidate memory after any task
2331
- workersToDispatch.push('consolidate');
2332
-
2333
- // Security-related task → dispatch audit worker
2334
- if (/\b(security|auth|vuln|cve|threat|token|permission|crypto)\b/.test(taskDesc)) {
2335
- workersToDispatch.push('audit');
2336
- }
2337
- // Performance-related → dispatch benchmark worker
2338
- if (/\b(performance|optimiz|benchmark|latency|throughput)\b/.test(taskDesc)) {
2339
- workersToDispatch.push('benchmark');
2340
- }
2341
- // Code changes → dispatch testgaps worker
2342
- if (/\b(implement|feature|refactor|fix|build|add|create|modify)\b/.test(taskDesc)) {
2343
- workersToDispatch.push('testgaps');
2344
- }
2345
- // Any significant task → dispatch map worker for codebase indexing
2346
- if (taskDesc.length > 50) {
2347
- workersToDispatch.push('map');
2348
- }
2349
-
2350
- // Dispatch via @monomind/hooks if available, otherwise write dispatch file
2351
- if (workersToDispatch.length > 0) {
2352
- var dispatchDir = path.join(CWD, '.monomind', 'worker-dispatch');
2353
- fs.mkdirSync(dispatchDir, { recursive: true });
2354
- var dispatchPayload = {
2355
- workers: workersToDispatch,
2356
- trigger: 'post-task',
2357
- taskDesc: taskDesc.substring(0, 100),
2358
- success: taskSuccess,
2359
- timestamp: new Date().toISOString(),
2360
- };
2361
- fs.writeFileSync(
2362
- path.join(dispatchDir, 'pending-' + Date.now() + '-' + Math.random().toString(36).slice(2, 7) + '.json'),
2363
- JSON.stringify(dispatchPayload), 'utf-8'
2364
- );
2365
- console.log('[WORKER_DISPATCH] Queued: ' + workersToDispatch.join(', '));
2366
- }
2367
- } catch (e) { /* non-fatal */ }
2368
-
2369
- // ── ADR Auto-Generation ────────────────────────────────────────────────
2370
- // When adr.autoGenerate is true and task involved architect-level work,
2371
- // create an ADR stub in the configured directory
2372
- try {
2373
- var settingsPath = path.join(CWD, '.claude', 'settings.json');
2374
- var adrCfg = {};
2375
- if (fs.existsSync(settingsPath)) {
2376
- var s = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
2377
- adrCfg = (s.monomind && s.monomind.adr) || {};
2378
- }
2379
- if (adrCfg.autoGenerate) {
2380
- var taskAgent = hookInput.agentSlug || hookInput.agent_slug || '';
2381
- var taskDescAdr = (typeof prompt === 'string' ? prompt : hookInput.description || '').toLowerCase();
2382
- var isArchitectLevel = ['architect', 'system-architect', 'software-architect'].includes(taskAgent)
2383
- || /\b(architecture|design decision|adr|trade-?off|migration strategy)\b/.test(taskDescAdr);
2384
- if (isArchitectLevel && taskDescAdr.length > 30) {
2385
- var adrDir = path.join(CWD, adrCfg.directory || 'docs/adrs');
2386
- fs.mkdirSync(adrDir, { recursive: true });
2387
- var adrNum = (fs.readdirSync(adrDir).filter(function(f) { return f.endsWith('.md'); }).length + 1)
2388
- .toString().padStart(4, '0');
2389
- var adrTitle = taskDescAdr.substring(0, 60).replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
2390
- var adrFile = path.join(adrDir, 'ADR-' + adrNum + '-' + adrTitle + '.md');
2391
- if (!fs.existsSync(adrFile)) {
2392
- var adrContent = '# ADR-' + adrNum + ': ' + (typeof prompt === 'string' ? prompt.substring(0, 80) : adrTitle) + '\n\n'
2393
- + '**Date:** ' + new Date().toISOString().slice(0, 10) + '\n'
2394
- + '**Status:** Accepted\n'
2395
- + '**Agent:** ' + (taskAgent || 'unknown') + '\n\n'
2396
- + '## Context\n\nAuto-generated from task completion.\n\n'
2397
- + '## Decision\n\n_Fill in the decision made._\n\n'
2398
- + '## Consequences\n\n_Fill in the consequences._\n';
2399
- fs.writeFileSync(adrFile, adrContent, 'utf-8');
2400
- console.log('[ADR_GENERATED] ' + path.basename(adrFile));
2401
- }
2402
- }
2403
- }
2404
- } catch (e) { /* non-fatal */ }
2405
243
 
2406
- console.log('[OK] Task completed');
244
+ 'post-task': async () => {
245
+ const h = require('./handlers/task-handler.cjs');
246
+ await h.handlePostTask(hCtx);
2407
247
  },
2408
248
 
249
+
2409
250
  'compact-manual': async () => {
2410
251
  if (intelligence && intelligence.consolidate) {
2411
252
  try { await runWithTimeout(function() { return intelligence.consolidate(); }, 'intelligence.consolidate()'); } catch (e) { /* non-fatal */ }