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