@monoes/monomindcli 1.10.29 → 1.10.31

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 (111) hide show
  1. package/.claude/helpers/auto-memory-hook.mjs +39 -4
  2. package/.claude/helpers/handlers/adr-draft-handler.cjs +64 -0
  3. package/.claude/helpers/handlers/agent-start-handler.cjs +99 -0
  4. package/.claude/helpers/handlers/edit-handler.cjs +145 -0
  5. package/.claude/helpers/handlers/graph-status-handler.cjs +38 -0
  6. package/.claude/helpers/handlers/route-handler.cjs +393 -0
  7. package/.claude/helpers/handlers/session-handler.cjs +167 -0
  8. package/.claude/helpers/handlers/session-restore-handler.cjs +348 -0
  9. package/.claude/helpers/handlers/task-handler.cjs +329 -0
  10. package/.claude/helpers/hook-handler.cjs +120 -2431
  11. package/.claude/helpers/intelligence.cjs +21 -2
  12. package/.claude/helpers/learning-service.mjs +166 -8
  13. package/.claude/helpers/memory-palace.cjs +72 -12
  14. package/.claude/helpers/router.cjs +79 -5
  15. package/.claude/helpers/statusline.cjs +193 -399
  16. package/.claude/helpers/utils/micro-agents.cjs +338 -0
  17. package/.claude/helpers/utils/monograph.cjs +349 -0
  18. package/.claude/helpers/utils/telemetry.cjs +144 -0
  19. package/.claude/skills/agent-browser-testing/SKILL.md +3 -2
  20. package/.claude/skills/monomind/browse-agentcore.md +116 -0
  21. package/.claude/skills/monomind/browse-electron.md +189 -0
  22. package/.claude/skills/monomind/browse-qa.md +229 -0
  23. package/.claude/skills/monomind/browse-references/authentication.md +162 -0
  24. package/.claude/skills/monomind/browse-references/trust-boundaries.md +41 -0
  25. package/.claude/skills/monomind/browse-references/video-recording.md +84 -0
  26. package/.claude/skills/monomind/browse-slack.md +189 -0
  27. package/.claude/skills/monomind/browse-vercel.md +240 -0
  28. package/.claude/skills/monomind/browse.md +724 -0
  29. package/dist/src/browser/actions.d.ts +28 -0
  30. package/dist/src/browser/actions.d.ts.map +1 -0
  31. package/dist/src/browser/actions.js +292 -0
  32. package/dist/src/browser/actions.js.map +1 -0
  33. package/dist/src/browser/batch.d.ts +13 -0
  34. package/dist/src/browser/batch.d.ts.map +1 -0
  35. package/dist/src/browser/batch.js +11 -0
  36. package/dist/src/browser/batch.js.map +1 -0
  37. package/dist/src/browser/browser.d.ts +14 -0
  38. package/dist/src/browser/browser.d.ts.map +1 -0
  39. package/dist/src/browser/browser.js +198 -0
  40. package/dist/src/browser/browser.js.map +1 -0
  41. package/dist/src/browser/cdp.d.ts +17 -0
  42. package/dist/src/browser/cdp.d.ts.map +1 -0
  43. package/dist/src/browser/cdp.js +106 -0
  44. package/dist/src/browser/cdp.js.map +1 -0
  45. package/dist/src/browser/console-log.d.ts +22 -0
  46. package/dist/src/browser/console-log.d.ts.map +1 -0
  47. package/dist/src/browser/console-log.js +55 -0
  48. package/dist/src/browser/console-log.js.map +1 -0
  49. package/dist/src/browser/dialog.d.ts +11 -0
  50. package/dist/src/browser/dialog.d.ts.map +1 -0
  51. package/dist/src/browser/dialog.js +36 -0
  52. package/dist/src/browser/dialog.js.map +1 -0
  53. package/dist/src/browser/emulation.d.ts +15 -0
  54. package/dist/src/browser/emulation.d.ts.map +1 -0
  55. package/dist/src/browser/emulation.js +62 -0
  56. package/dist/src/browser/emulation.js.map +1 -0
  57. package/dist/src/browser/find.d.ts +21 -0
  58. package/dist/src/browser/find.d.ts.map +1 -0
  59. package/dist/src/browser/find.js +118 -0
  60. package/dist/src/browser/find.js.map +1 -0
  61. package/dist/src/browser/index.d.ts +18 -0
  62. package/dist/src/browser/index.d.ts.map +1 -0
  63. package/dist/src/browser/index.js +18 -0
  64. package/dist/src/browser/index.js.map +1 -0
  65. package/dist/src/browser/network.d.ts +11 -0
  66. package/dist/src/browser/network.d.ts.map +1 -0
  67. package/dist/src/browser/network.js +81 -0
  68. package/dist/src/browser/network.js.map +1 -0
  69. package/dist/src/browser/pdf.d.ts +15 -0
  70. package/dist/src/browser/pdf.d.ts.map +1 -0
  71. package/dist/src/browser/pdf.js +27 -0
  72. package/dist/src/browser/pdf.js.map +1 -0
  73. package/dist/src/browser/screenshot.d.ts +15 -0
  74. package/dist/src/browser/screenshot.d.ts.map +1 -0
  75. package/dist/src/browser/screenshot.js +36 -0
  76. package/dist/src/browser/screenshot.js.map +1 -0
  77. package/dist/src/browser/session.d.ts +8 -0
  78. package/dist/src/browser/session.d.ts.map +1 -0
  79. package/dist/src/browser/session.js +50 -0
  80. package/dist/src/browser/session.js.map +1 -0
  81. package/dist/src/browser/snapshot.d.ts +12 -0
  82. package/dist/src/browser/snapshot.d.ts.map +1 -0
  83. package/dist/src/browser/snapshot.js +147 -0
  84. package/dist/src/browser/snapshot.js.map +1 -0
  85. package/dist/src/browser/storage.d.ts +11 -0
  86. package/dist/src/browser/storage.d.ts.map +1 -0
  87. package/dist/src/browser/storage.js +43 -0
  88. package/dist/src/browser/storage.js.map +1 -0
  89. package/dist/src/browser/tabs.d.ts +8 -0
  90. package/dist/src/browser/tabs.d.ts.map +1 -0
  91. package/dist/src/browser/tabs.js +25 -0
  92. package/dist/src/browser/tabs.js.map +1 -0
  93. package/dist/src/browser/types.d.ts +109 -0
  94. package/dist/src/browser/types.d.ts.map +1 -0
  95. package/dist/src/browser/types.js +16 -0
  96. package/dist/src/browser/types.js.map +1 -0
  97. package/dist/src/browser/wait.d.ts +4 -0
  98. package/dist/src/browser/wait.d.ts.map +1 -0
  99. package/dist/src/browser/wait.js +122 -0
  100. package/dist/src/browser/wait.js.map +1 -0
  101. package/dist/src/commands/browse.d.ts +8 -0
  102. package/dist/src/commands/browse.d.ts.map +1 -0
  103. package/dist/src/commands/browse.js +1494 -0
  104. package/dist/src/commands/browse.js.map +1 -0
  105. package/dist/src/commands/index.d.ts.map +1 -1
  106. package/dist/src/commands/index.js +2 -0
  107. package/dist/src/commands/index.js.map +1 -1
  108. package/dist/src/ui/dashboard-v2.html +1857 -0
  109. package/dist/src/ui/server.mjs +71 -1
  110. package/dist/tsconfig.tsbuildinfo +1 -1
  111. package/package.json +2 -1
@@ -0,0 +1,393 @@
1
+ 'use strict';
2
+ // Extracted from hook-handler.cjs — receives hCtx from dispatcher.
3
+ // Behavioral equivalence verified: 133 routing tests pass post-extraction.
4
+ // hCtx (hook context) contains all shared state and utility functions:
5
+ // hCtx.hookInput, hCtx.toolInput, hCtx.toolName, hCtx.prompt, hCtx.args, hCtx.CWD
6
+ // hCtx.session, hCtx.router, hCtx.intelligence
7
+ // hCtx.isSimpleCommand — function defined in main(), passed via hCtx
8
+ // hCtx.getLearningService — async factory for LearningService singleton
9
+ // Utility fns: _recordRecentEdit, _findAffectedTests, _recordHookLatency,
10
+ // _getBudgetStatus, _injectCompactGraphMap, _maybeRebuildMonograph,
11
+ // _buildKnowledgeSearchFn, getMonographSuggestions, getMonographNeighbors,
12
+ // runWithTimeout, safeRequire, scanMicroAgentTriggers, _recordGraphTelemetry,
13
+ // _recordDecisionMarkers, _recordToolCall, _openMonographDb, fs, path
14
+ //
15
+ // NOTE: The 'route' handler has a local variable named 'ctx' (from intelligence.getContext).
16
+ // The dispatcher passes the hook context as 'hCtx' to avoid collision.
17
+
18
+ const path = require('path');
19
+ const fs = require('fs');
20
+
21
+ module.exports = {
22
+ handle: async function(hCtx) {
23
+ var prompt = hCtx.prompt;
24
+ var hookInput = hCtx.hookInput;
25
+ var router = hCtx.router;
26
+ var intelligence = hCtx.intelligence;
27
+ var CWD = hCtx.CWD;
28
+
29
+ // For slash commands and single-action invocations: skip routing panel output
30
+ // but still write last-route.json so the statusline reflects the current action.
31
+ if (hCtx.isSimpleCommand(prompt)) {
32
+ try {
33
+ var cmdLabel = (typeof prompt === 'string' && prompt.trim().startsWith('/'))
34
+ ? prompt.trim().split(/\s+/)[0] // e.g. "/ts"
35
+ : (hookInput.commandName || hookInput.command_name || 'command');
36
+ var routeDir = path.join(CWD, '.monomind');
37
+ fs.mkdirSync(routeDir, { recursive: true });
38
+ fs.writeFileSync(
39
+ path.join(routeDir, 'last-route.json'),
40
+ JSON.stringify({
41
+ agent: cmdLabel,
42
+ confidence: 1.0,
43
+ reason: 'predefined command — no routing needed',
44
+ semanticRouting: false,
45
+ updatedAt: new Date().toISOString(),
46
+ }),
47
+ 'utf-8'
48
+ );
49
+ } catch (e) { /* non-fatal */ }
50
+ return;
51
+ }
52
+
53
+ if (intelligence && intelligence.getContext) {
54
+ try {
55
+ const ctx = intelligence.getContext(prompt);
56
+ if (ctx) console.log(ctx);
57
+ } catch (e) { /* non-fatal */ }
58
+ }
59
+ if (router && (router.routeTaskSemantic || router.routeTask)) {
60
+ const routeFn = router.routeTaskSemantic || router.routeTask;
61
+ var result = await Promise.resolve(routeFn(prompt));
62
+
63
+ // Graph-fallback override: when the router picked a low-confidence
64
+ // non-dev specialist (marketing slugs etc) but monograph has a strong
65
+ // graph match for the prompt, derive the agent from the top file's
66
+ // label instead. Stops "improve the system" → China E-Commerce.
67
+ try {
68
+ // Don't override when the prompt has obvious non-dev keywords —
69
+ // marketing/sales/finance asks SHOULD route to those specialists.
70
+ 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);
71
+
72
+ 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;
73
+ var pickedDev = devAgents.test(String(result.agent || '').trim()) ||
74
+ devAgents.test(String(result.agentSlug || '').trim());
75
+
76
+ var resConf = (result.confidence != null ? result.confidence : 0);
77
+ var resReason = String(result.reason || '');
78
+ var fromKeywordStage = resReason.indexOf('Keyword 2-stage') !== -1;
79
+ 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);
80
+
81
+ var shouldOverride = !nonDevPrompt && (
82
+ (!pickedDev && resConf < 0.85) ||
83
+ (fromKeywordStage && promptIsDevish)
84
+ );
85
+ if (shouldOverride) {
86
+ var topGraph = hCtx.getMonographSuggestions(prompt, 1)[0];
87
+ if (topGraph) {
88
+ var agent = 'coder';
89
+ var file = (topGraph.file || '').toLowerCase();
90
+ // Test files
91
+ if (/\.(test|spec)\./.test(file) || file.includes('__tests__')) agent = 'tester';
92
+ // Architecture/system docs → architect
93
+ else if (/(architect|adr-|design-doc|rfc-)/.test(file)) agent = 'system-architect';
94
+ // Pure docs → tech writer
95
+ else if (file.endsWith('readme.md') || file.startsWith('docs/') || /\/docs\//.test(file)) agent = 'Technical Writer';
96
+ // Other .md (skills, agents, configs) → coder (they're code-adjacent)
97
+ else if (file.endsWith('.md')) agent = 'coder';
98
+ // Class/Interface → architect
99
+ else if (topGraph.label === 'Class' || topGraph.label === 'Interface') agent = 'system-architect';
100
+ // Functions, files, methods → coder
101
+ else agent = 'coder';
102
+ // Scale confidence by graph degree: well-connected nodes are stronger anchors.
103
+ var topDeg = topGraph.deg || 0;
104
+ var graphConf = topDeg > 30 ? 0.80 : (topDeg > 10 ? 0.75 : 0.70);
105
+ result = Object.assign({}, result, {
106
+ agent: agent,
107
+ agentSlug: agent,
108
+ confidence: graphConf,
109
+ reason: 'Graph fallback: top file ' + (topGraph.name || '').substring(0, 30) + ' [' + topGraph.label + '] deg=' + topDeg,
110
+ specificAgents: [],
111
+ extrasMatches: [],
112
+ });
113
+ }
114
+ }
115
+ } catch (e) {}
116
+
117
+ var output = [];
118
+ output.push('[INFO] Routing task: ' + (prompt.substring(0, 80) || '(no prompt)'));
119
+ output.push('');
120
+ // Suppress the agent recommendation panel for low-confidence routes on
121
+ // short prompts — the recommendation is almost always wrong (e.g.
122
+ // "what else can we do" → marketing → China E-Commerce). Saves ~150
123
+ // tokens per prompt. Skill matches and specific agents still render
124
+ // below when confidence is decent.
125
+ var conf = result.confidence != null ? result.confidence : 0;
126
+ var promptShort = (prompt || '').trim().length < 60;
127
+ var lowConf = conf < 0.70;
128
+ var suppressPanel = lowConf && promptShort;
129
+ if (!suppressPanel) {
130
+ output.push('+------------- monomind | Primary Recommendation --------------+');
131
+ output.push('| Agent: ' + (result.agent || 'unknown').substring(0, 54).padEnd(54) + '|');
132
+ output.push('| Confidence: ' + ((result.confidence != null ? (result.confidence * 100).toFixed(1) : '?') + '%').padEnd(49) + '|');
133
+ output.push('| Reason: ' + (result.reason || '').substring(0, 53).padEnd(53) + '|');
134
+ output.push('+--------------------------------------------------------------+');
135
+ }
136
+
137
+ // ── Persist routing result for statusline display ─────────────
138
+ try {
139
+ var routeDir = path.join(CWD, '.monomind');
140
+ fs.mkdirSync(routeDir, { recursive: true });
141
+ // Always use the resolved agent name — never persist "extras"
142
+ var resolvedAgent = result.agent;
143
+ if (!resolvedAgent || resolvedAgent === 'extras') {
144
+ var topExtra = result.extrasMatches && result.extrasMatches[0];
145
+ resolvedAgent = topExtra ? topExtra.name : 'Specialist Agent';
146
+ }
147
+ var routePayload = {
148
+ agent: resolvedAgent,
149
+ agentSlug: result.agentSlug || null,
150
+ confidence: result.confidence,
151
+ reason: result.reason,
152
+ semanticRouting: result.semanticRouting || false,
153
+ llmRouting: result.llmRouting || false,
154
+ updatedAt: new Date().toISOString(),
155
+ };
156
+ if (result.extrasMatches && result.extrasMatches.length > 0) {
157
+ routePayload.extrasMatches = result.extrasMatches.map(function(e) {
158
+ return { name: e.name, slug: e.slug, category: e.category };
159
+ });
160
+ }
161
+ fs.writeFileSync(
162
+ path.join(routeDir, 'last-route.json'),
163
+ JSON.stringify(routePayload),
164
+ 'utf-8'
165
+ );
166
+ } catch (e) { /* non-fatal */ }
167
+
168
+ // ── Dev skill suggestions ──────────────────────────────────────
169
+ var matches = result.skillMatches || [];
170
+ if (matches.length > 0) {
171
+ // Check for high-confidence auto-invoke: if top skill scored >= 3 keyword
172
+ // hits and is the dominant match, auto-invoke instead of just suggesting
173
+ var topMatch = matches[0];
174
+ var autoInvoke = false;
175
+ if (topMatch && topMatch.score >= 3 && matches.length <= 2) {
176
+ autoInvoke = true;
177
+ } else if (topMatch && topMatch.score >= 2 && matches.length === 1 && (result.confidence ?? 0) < 0.7) {
178
+ // Single strong skill match with weak agent routing = skill should take over
179
+ autoInvoke = true;
180
+ }
181
+
182
+ if (autoInvoke) {
183
+ output.push('');
184
+ output.push('+======== SKILL AUTO-ACTIVATED (high confidence match) ========+');
185
+ output.push('| ' + topMatch.invoke.substring(0, 61).padEnd(61) + '|');
186
+ output.push('| INSTRUCTION: Invoke ' + topMatch.invoke.substring(0, 41).padEnd(41) + '|');
187
+ output.push('| BEFORE responding. This skill matched with very high |');
188
+ output.push('| confidence — do not skip it. |');
189
+ output.push('+==============================================================+');
190
+ } else {
191
+ output.push('');
192
+ if ((result.confidence ?? 0) < 0.8) {
193
+ output.push('+----------- Skill Suggestions (pick one if relevant) ---------+');
194
+ output.push('| No strong primary match — here are the best skill candidates |');
195
+ } else {
196
+ output.push('+----------- Matching Skills (invoke via Skill tool) ----------+');
197
+ }
198
+ matches.forEach(function(m, i) {
199
+ var label = (i + 1) + '. ' + m.skill;
200
+ var desc = (m.description || '').substring(0, 30);
201
+ var line = '| ' + label.substring(0, 30).padEnd(30) + desc.padEnd(30) + ' |';
202
+ output.push(line);
203
+ output.push('| invoke: ' + m.invoke.substring(0, 51).padEnd(51) + '|');
204
+ });
205
+ output.push('+--------------------------------------------------------------+');
206
+ if ((result.confidence ?? 0) < 0.8) {
207
+ output.push('| To use a skill: call Skill("skill-name") before responding. |');
208
+ output.push('+--------------------------------------------------------------+');
209
+ }
210
+ }
211
+ }
212
+
213
+ // ── Specific agent panel ──────────────────────────────────────────────────
214
+ // Skip entirely on suppressed (low-confidence + short) prompts.
215
+ var specificAgents = result.specificAgents || [];
216
+ if (specificAgents.length > 0 && !suppressPanel) {
217
+ output.push('');
218
+ var saHdr = '------- Specific Agents (' + specificAgents.length + ' available) ';
219
+ output.push('+' + saHdr + '-'.repeat(Math.max(1, 62 - saHdr.length)) + '+');
220
+ specificAgents.forEach(function(a, i) {
221
+ var label = (i + 1) + '. ' + a.label;
222
+ var note = (a.note || '').substring(0, 26);
223
+ output.push('| ' + label.substring(0, 33).padEnd(33) + note.padEnd(27) + ' |');
224
+ if (a.slug) {
225
+ output.push('| slug: ' + a.slug.substring(0, 52).padEnd(52) + ' |');
226
+ }
227
+ });
228
+ output.push('+--------------------------------------------------------------+');
229
+ output.push('| Use: Task({ subagent_type: "<slug>" }) or /specialagent |');
230
+ output.push('+--------------------------------------------------------------+');
231
+ }
232
+
233
+ // ── Specialist agents (non-dev domain) — only shown when specificAgents panel wasn't shown ──
234
+ var extras = result.extrasMatches || [];
235
+ var specificAgentsShown = (result.specificAgents || []).length > 0;
236
+ if (extras.length > 0 && !specificAgentsShown && !suppressPanel) {
237
+ output.push('');
238
+ var spHdr = '------- Specialist Agents (' + extras.length + ' matched) ';
239
+ output.push('+' + spHdr + '-'.repeat(Math.max(1, 62 - spHdr.length)) + '+');
240
+ extras.slice(0, 5).forEach(function(e, i) {
241
+ var label = (i + 1) + '. ' + e.name;
242
+ var cat = '[' + e.category + ']';
243
+ output.push('| ' + label.substring(0, 44).padEnd(44) + cat.substring(0, 16).padEnd(16) + ' |');
244
+ output.push('| slug: ' + e.slug.substring(0, 52).padEnd(52) + ' |');
245
+ });
246
+ output.push('+--------------------------------------------------------------+');
247
+ output.push('| Use: Task({ subagent_type: "<slug>" }) or /specialagent |');
248
+ output.push('+--------------------------------------------------------------+');
249
+ }
250
+
251
+ // ── MicroAgent Trigger Scan (Task 32) ──────────────────────────────
252
+ try {
253
+ var triggerResult = hCtx.scanMicroAgentTriggers(typeof prompt === 'string' ? prompt : '');
254
+ if (triggerResult.matches.length > 0) {
255
+ output.push('');
256
+ if (triggerResult.takeoverAgent) {
257
+ var tAgent = triggerResult.takeoverAgent;
258
+ var tKw = triggerResult.matches[0].matchedText;
259
+ output.push('+============= MicroAgent TAKEOVER Detected ===================+');
260
+ output.push('| Specialist: ' + tAgent.substring(0, 49).padEnd(49) + '|');
261
+ output.push('| Keyword: ' + ('"' + tKw + '"').substring(0, 49).padEnd(49) + '|');
262
+ output.push('| Recommended: use this specialist instead of primary agent. |');
263
+ output.push('+==============================================================+');
264
+ } else {
265
+ output.push('+------- MicroAgent Specialists Triggered ---------------------+');
266
+ triggerResult.matches.forEach(function(m) {
267
+ var slug = m.agentSlug.substring(0, 37).padEnd(37);
268
+ var kw = ('(match: "' + m.matchedText + '")').substring(0, 21).padEnd(21);
269
+ output.push('| + ' + slug + kw + ' |');
270
+ });
271
+ output.push('+--------------------------------------------------------------+');
272
+ }
273
+ // Persist trigger matches alongside route result
274
+ try {
275
+ var routeFile = path.join(CWD, '.monomind', 'last-route.json');
276
+ var existing = JSON.parse(fs.readFileSync(routeFile, 'utf-8'));
277
+ existing.microAgents = { injectAgents: triggerResult.injectAgents || [], takeoverAgent: triggerResult.takeoverAgent || null };
278
+ fs.writeFileSync(routeFile, JSON.stringify(existing), 'utf-8');
279
+ } catch (e) {}
280
+ }
281
+ } catch (e) { /* non-fatal */ }
282
+
283
+ console.log(output.join('\n'));
284
+
285
+ // Record any decision markers in this prompt (auto-ADR pipeline).
286
+ try { hCtx._recordDecisionMarkers(prompt); } catch (e) {}
287
+
288
+ // Cost budget — emit amber/red banner when approaching limit.
289
+ try {
290
+ var budget = hCtx._getBudgetStatus();
291
+ if (budget && budget.alert) {
292
+ var tunedNote = budget.autoTuned ? ' (auto-tuned)' : '';
293
+ if (budget.spike && !budget.breached) {
294
+ console.log('[BUDGET_SPIKE] Today $' + budget.todayCost.toFixed(2) + ' is >2x your rolling daily avg. Unusual spend — review .monomind/metrics/token-summary.json.');
295
+ } else if (budget.breached) {
296
+ 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.');
297
+ } else {
298
+ console.log('[BUDGET_ALERT] Daily ' + budget.dailyPct + '% of $' + budget.dailyLimit + ' · Monthly ' + budget.monthlyPct + '% of $' + budget.monthlyLimit + tunedNote + '.');
299
+ }
300
+ }
301
+ } catch (e) {}
302
+
303
+ // Inject monograph hint for complex tasks.
304
+ // Source of truth is .monomind/monograph.db (SQLite). Legacy stats.json
305
+ // is no longer written by the build, so it is checked only as a fallback.
306
+ try {
307
+ var monographDb = path.join(CWD, '.monomind', 'monograph.db');
308
+ var legacyStats = path.join(CWD, '.monomind', 'graph', 'stats.json');
309
+ var nodeCount = 0;
310
+ if (fs.existsSync(monographDb)) {
311
+ try {
312
+ var hintDb = hCtx._openMonographDb();
313
+ if (hintDb) {
314
+ nodeCount = hintDb.prepare('SELECT COUNT(*) AS c FROM nodes').get().c;
315
+ }
316
+ } catch (e) { /* ignore — fall back to legacy */ }
317
+ }
318
+ if (nodeCount === 0 && fs.existsSync(legacyStats)) {
319
+ try {
320
+ var gStats = JSON.parse(fs.readFileSync(legacyStats, 'utf-8'));
321
+ nodeCount = gStats.nodes || 0;
322
+ } catch (e) { /* ignore */ }
323
+ }
324
+ if (nodeCount > 100) {
325
+ // Pre-resolve top-5 relevant files for the user's prompt — the LLM
326
+ // sees the answer inline instead of being told to call a tool.
327
+ var suggestions = hCtx.getMonographSuggestions(prompt, 5);
328
+
329
+ // Boost recently-edited files to the top of pre-resolve suggestions.
330
+ // Even when the FTS index hasn't caught up to the latest edits, the
331
+ // LLM should see the files it just modified as the primary context.
332
+ try {
333
+ var recentEditsForRoute = hCtx._getRecentEdits();
334
+ if (recentEditsForRoute.length > 0) {
335
+ // Extract prompt keywords for relevance gating
336
+ var promptWords = (prompt || '').toLowerCase().match(/[a-z][a-z0-9_-]{2,}/g) || [];
337
+ var promptWordSet = {};
338
+ for (var pw = 0; pw < promptWords.length; pw++) promptWordSet[promptWords[pw]] = 1;
339
+
340
+ var existingFiles = {};
341
+ for (var se = 0; se < suggestions.length; se++) existingFiles[suggestions[se].file || ''] = 1;
342
+
343
+ var editBoosts = [];
344
+ for (var re = 0; re < recentEditsForRoute.length && editBoosts.length < 2; re++) {
345
+ var reFile = recentEditsForRoute[re].file;
346
+ // Skip if already in suggestions
347
+ if (existingFiles[reFile]) continue;
348
+ var reName = path.basename(reFile, path.extname(reFile)).toLowerCase();
349
+ // Only boost if filename shares a keyword with the prompt OR the edit is very recent (<3 min)
350
+ var veryRecent = (Date.now() - recentEditsForRoute[re].editedAt) < 3 * 60 * 1000;
351
+ var editMatches = promptWordSet[reName] || veryRecent;
352
+ if (editMatches) {
353
+ editBoosts.push({ name: path.basename(reFile), label: 'File', file: reFile, deg: 0, _editBoost: true });
354
+ }
355
+ }
356
+ if (editBoosts.length > 0) {
357
+ suggestions = editBoosts.concat(suggestions).slice(0, 5);
358
+ }
359
+ }
360
+ } catch (e) { /* non-fatal */ }
361
+
362
+ if (suggestions.length > 0) {
363
+ console.log('\n[MONOGRAPH] ' + nodeCount + ' nodes indexed. Top files for this task (pre-resolved from graph):');
364
+ for (var si = 0; si < suggestions.length; si++) {
365
+ var s = suggestions[si];
366
+ var editTag = s._editBoost ? ' ✎' : '';
367
+ console.log(' · ' + s.name + ' [' + s.label + '] — ' + (s.file || '') + (s.deg ? ' (deg ' + s.deg + ')' : '') + editTag);
368
+ }
369
+ console.log(' Use mcp__monomind__monograph_query / monograph_impact for deeper drill-down.');
370
+ hCtx._recordGraphTelemetry('preresolve_hit');
371
+ } else {
372
+ console.log('\n[MONOGRAPH] ' + nodeCount + ' nodes indexed. Call mcp__monomind__monograph_suggest first to find relevant files without grepping.');
373
+ hCtx._recordGraphTelemetry('preresolve_miss');
374
+ }
375
+ }
376
+ } catch(e) {}
377
+
378
+ // Swarm mode selection is available on-demand via /mastermind slash command.
379
+ } else {
380
+ console.log('[INFO] Router not available, using default routing');
381
+ }
382
+
383
+ // Task 22: TeamRoutingModes — only log when an explicit swarm config is present
384
+ try {
385
+ var swarmCfgPath = path.join(CWD, '.monomind', 'swarm-config.json');
386
+ if (fs.existsSync(swarmCfgPath)) {
387
+ var topology22 = JSON.parse(fs.readFileSync(swarmCfgPath, 'utf-8')).topology || 'mesh';
388
+ var mode22 = topology22 === 'hierarchical' ? 'route' : 'coordinate';
389
+ console.log('[ROUTING_MODE] topology=' + topology22 + ' → mode=' + mode22);
390
+ }
391
+ } catch (e) { /* non-fatal */ }
392
+ }
393
+ };
@@ -0,0 +1,167 @@
1
+ 'use strict';
2
+ // Extracted from hook-handler.cjs — receives hCtx from dispatcher.
3
+ // Handles 'session-end' hook event.
4
+ // See route-handler.cjs for full hCtx field documentation.
5
+
6
+ const path = require('path');
7
+ const fs = require('fs');
8
+
9
+ module.exports = {
10
+ handleEnd: async function(hCtx) {
11
+ var hookInput = hCtx.hookInput;
12
+ var intelligence = hCtx.intelligence;
13
+ var session = hCtx.session;
14
+ var CWD = hCtx.CWD;
15
+
16
+ // Check if daemon is holding the consolidation lock — if so, skip synchronous consolidation
17
+ // to avoid duplicate work and potential index corruption
18
+ var consolidationLockPath = path.join(CWD, '.monomind', 'consolidation.lock');
19
+ var daemonHoldsLock = fs.existsSync(consolidationLockPath);
20
+
21
+ if (daemonHoldsLock) {
22
+ console.log('[SESSION] Skipping consolidation — daemon holds lock');
23
+ }
24
+
25
+ // Consolidate intelligence (with timeout — #1530)
26
+ if (!daemonHoldsLock && intelligence && intelligence.consolidate) {
27
+ var consResult = await hCtx.runWithTimeout(function() { return intelligence.consolidate(); }, 'intelligence.consolidate()');
28
+ if (consResult && consResult.entries > 0) {
29
+ var msg = '[INTELLIGENCE] Consolidated: ' + consResult.entries + ' entries, ' + consResult.edges + ' edges';
30
+ if (consResult.newEntries > 0) msg += ', ' + consResult.newEntries + ' new';
31
+ msg += ', PageRank recomputed';
32
+ console.log(msg);
33
+ }
34
+ }
35
+ try {
36
+ if (session && session.end) {
37
+ session.end();
38
+ } else {
39
+ console.log('[OK] Session ended');
40
+ }
41
+ } catch (e) { console.log('[WARN] Session end failed: ' + e.message); }
42
+
43
+ // ── Routing Feedback Loop (SE-001) ────────────────────────────────────
44
+ // Persist routing accuracy feedback so the router improves over sessions.
45
+ // sessionSuccess is derived from intelligence-outcomes.jsonl entries written
46
+ // during this session (last 30 minutes). A session is marked failed when the
47
+ // majority of feedback() calls carried success=false in that window.
48
+ try {
49
+ var feedbackPath = path.join(CWD, '.monomind', 'routing-feedback.jsonl');
50
+ var lastRoutePath = path.join(CWD, '.monomind', 'last-route.json');
51
+ if (fs.existsSync(lastRoutePath)) {
52
+ var lastRoute = JSON.parse(fs.readFileSync(lastRoutePath, 'utf-8'));
53
+
54
+ // Derive sessionSuccess from intelligence-outcomes.jsonl
55
+ var sessionSuccess = true; // optimistic default when no signal exists
56
+ try {
57
+ var outcomesPath = path.join(CWD, '.monomind', 'intelligence-outcomes.jsonl');
58
+ if (fs.existsSync(outcomesPath)) {
59
+ var windowMs = 30 * 60 * 1000; // 30-minute session window
60
+ var cutoff = Date.now() - windowMs;
61
+ var outcomeLines = fs.readFileSync(outcomesPath, 'utf-8').trim().split('\n').filter(Boolean);
62
+ var recent = outcomeLines.map(function(l) {
63
+ try { return JSON.parse(l); } catch { return null; }
64
+ }).filter(function(e) { return e && e.ts && e.ts >= cutoff; });
65
+ if (recent.length > 0) {
66
+ var failures = recent.filter(function(e) { return e.success === false; }).length;
67
+ // Majority-vote: session fails only if more than half the recent signals are failures
68
+ sessionSuccess = failures / recent.length < 0.5;
69
+ }
70
+ }
71
+ } catch (e) { /* non-critical — keep optimistic default */ }
72
+
73
+ if (intelligence && intelligence.feedback) {
74
+ try { intelligence.feedback(sessionSuccess); } catch (e) { /* non-fatal */ }
75
+ }
76
+ var feedbackEntry = {
77
+ timestamp: new Date().toISOString(),
78
+ suggestedAgent: lastRoute.agent,
79
+ confidence: lastRoute.confidence,
80
+ sessionId: hookInput.sessionId || hookInput.session_id || '',
81
+ intelligenceFeedback: sessionSuccess,
82
+ };
83
+ fs.appendFileSync(feedbackPath, JSON.stringify(feedbackEntry) + '\n', 'utf-8');
84
+ // Rotate: keep last 1000 lines to prevent unbounded growth
85
+ try {
86
+ var raw = fs.readFileSync(feedbackPath, 'utf-8');
87
+ var lines = raw.split('\n').filter(Boolean);
88
+ if (lines.length > 1000) {
89
+ fs.writeFileSync(feedbackPath, lines.slice(-1000).join('\n') + '\n', 'utf-8');
90
+ }
91
+ } catch (e2) { /* rotation is best-effort */ }
92
+ }
93
+ } catch (e) { /* non-fatal */ }
94
+
95
+ // Memory Palace tombstone writes removed — redundant with raw session JSONL
96
+
97
+ // ── Learning Service Auto-Consolidation ─────────────────────────────
98
+ // Consolidate learned patterns from short-term to long-term storage.
99
+ // Uses module-level singleton (getLearningService) so the DB is not
100
+ // reopened on every session-end — state accumulated during the session
101
+ // is preserved and consolidated in a single pass.
102
+ if (!daemonHoldsLock) {
103
+ try {
104
+ var ls = await hCtx.getLearningService();
105
+ if (ls && ls.consolidate) {
106
+ var lResult = await hCtx.runWithTimeout(function() { return ls.consolidate(); }, 'learning.consolidate()');
107
+ if (lResult && lResult.promoted > 0) {
108
+ console.log('[LEARNING] Consolidated: ' + lResult.promoted + ' patterns promoted to long-term');
109
+ }
110
+ }
111
+ if (ls && ls.promoteEpisodic) {
112
+ try {
113
+ var promResult = await hCtx.runWithTimeout(function() { return ls.promoteEpisodic(); }, 'learning.promoteEpisodic()');
114
+ if (promResult && promResult.promoted > 0) {
115
+ console.log('[LEARNING] Promoted ' + promResult.promoted + ' episodic patterns to semantic memory');
116
+ }
117
+ } catch (e) { /* non-fatal */ }
118
+ }
119
+ } catch (e) { /* non-fatal — learning-service may need better-sqlite3 */ }
120
+ }
121
+
122
+ // ── Context Persistence Auto-Archive ─────────────────────────────────
123
+ // Archive conversation context so it survives compaction and new sessions
124
+ try {
125
+ var cpHook = await import('file://' + path.join(__dirname, '..', 'context-persistence-hook.mjs'));
126
+ if (cpHook && cpHook.archive) {
127
+ await hCtx.runWithTimeout(function() { return cpHook.archive(); }, 'context-persistence.archive()');
128
+ console.log('[CONTEXT_PERSIST] Session transcript archived');
129
+ } else if (cpHook && cpHook.default && cpHook.default.archive) {
130
+ await hCtx.runWithTimeout(function() { return cpHook.default.archive(); }, 'context-persistence.archive()');
131
+ console.log('[CONTEXT_PERSIST] Session transcript archived');
132
+ }
133
+ } catch (e) { /* non-fatal — context-persistence may not export archive() */ }
134
+
135
+ // ── Worker Queue Cleanup ─────────────────────────────────────────────
136
+ // Process and clean up any pending worker dispatch files
137
+ try {
138
+ var dispatchDir = path.join(CWD, '.monomind', 'worker-dispatch');
139
+ if (fs.existsSync(dispatchDir)) {
140
+ var pending = fs.readdirSync(dispatchDir).filter(function(f) { return f.startsWith('pending-'); });
141
+ if (pending.length > 0) {
142
+ console.log('[WORKER_CLEANUP] ' + pending.length + ' worker dispatch(es) pending from this session');
143
+ }
144
+ // Move to processed
145
+ var processedDir = path.join(dispatchDir, 'processed');
146
+ fs.mkdirSync(processedDir, { recursive: true });
147
+ pending.forEach(function(f) {
148
+ try {
149
+ fs.renameSync(path.join(dispatchDir, f), path.join(processedDir, f));
150
+ } catch (e) { /* ignore */ }
151
+ });
152
+ // Trim processed/ to last 200 files to prevent unbounded growth
153
+ try {
154
+ var processedFiles = fs.readdirSync(processedDir)
155
+ .filter(function(f) { return f.startsWith('pending-'); })
156
+ .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 }; })
157
+ .sort(function(a, b) { return a.mt - b.mt; });
158
+ if (processedFiles.length > 200) {
159
+ processedFiles.slice(0, processedFiles.length - 200).forEach(function(item) {
160
+ try { fs.unlinkSync(path.join(processedDir, item.f)); } catch (e2) { /* ignore */ }
161
+ });
162
+ }
163
+ } catch (e2) { /* non-fatal */ }
164
+ }
165
+ } catch (e) { /* non-fatal */ }
166
+ }
167
+ };