@monoes/monomindcli 1.10.29 → 1.10.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/.claude/helpers/auto-memory-hook.mjs +39 -4
  2. package/.claude/helpers/handlers/edit-handler.cjs +145 -0
  3. package/.claude/helpers/handlers/route-handler.cjs +393 -0
  4. package/.claude/helpers/handlers/session-handler.cjs +167 -0
  5. package/.claude/helpers/handlers/session-restore-handler.cjs +343 -0
  6. package/.claude/helpers/handlers/task-handler.cjs +329 -0
  7. package/.claude/helpers/hook-handler.cjs +114 -2273
  8. package/.claude/helpers/intelligence.cjs +21 -2
  9. package/.claude/helpers/learning-service.mjs +166 -8
  10. package/.claude/helpers/memory-palace.cjs +72 -12
  11. package/.claude/helpers/router.cjs +79 -5
  12. package/.claude/helpers/statusline.cjs +193 -399
  13. package/.claude/helpers/utils/micro-agents.cjs +338 -0
  14. package/.claude/helpers/utils/monograph.cjs +349 -0
  15. package/.claude/helpers/utils/telemetry.cjs +144 -0
  16. package/.claude/skills/agent-browser-testing/SKILL.md +3 -2
  17. package/.claude/skills/monomind/browse-agentcore.md +116 -0
  18. package/.claude/skills/monomind/browse-electron.md +189 -0
  19. package/.claude/skills/monomind/browse-qa.md +229 -0
  20. package/.claude/skills/monomind/browse-references/authentication.md +162 -0
  21. package/.claude/skills/monomind/browse-references/trust-boundaries.md +41 -0
  22. package/.claude/skills/monomind/browse-references/video-recording.md +84 -0
  23. package/.claude/skills/monomind/browse-slack.md +189 -0
  24. package/.claude/skills/monomind/browse-vercel.md +240 -0
  25. package/.claude/skills/monomind/browse.md +724 -0
  26. package/dist/src/browser/actions.d.ts +13 -0
  27. package/dist/src/browser/actions.d.ts.map +1 -0
  28. package/dist/src/browser/actions.js +201 -0
  29. package/dist/src/browser/actions.js.map +1 -0
  30. package/dist/src/browser/browser.d.ts +14 -0
  31. package/dist/src/browser/browser.d.ts.map +1 -0
  32. package/dist/src/browser/browser.js +198 -0
  33. package/dist/src/browser/browser.js.map +1 -0
  34. package/dist/src/browser/cdp.d.ts +17 -0
  35. package/dist/src/browser/cdp.d.ts.map +1 -0
  36. package/dist/src/browser/cdp.js +106 -0
  37. package/dist/src/browser/cdp.js.map +1 -0
  38. package/dist/src/browser/index.d.ts +11 -0
  39. package/dist/src/browser/index.d.ts.map +1 -0
  40. package/dist/src/browser/index.js +11 -0
  41. package/dist/src/browser/index.js.map +1 -0
  42. package/dist/src/browser/network.d.ts +11 -0
  43. package/dist/src/browser/network.d.ts.map +1 -0
  44. package/dist/src/browser/network.js +81 -0
  45. package/dist/src/browser/network.js.map +1 -0
  46. package/dist/src/browser/screenshot.d.ts +15 -0
  47. package/dist/src/browser/screenshot.d.ts.map +1 -0
  48. package/dist/src/browser/screenshot.js +36 -0
  49. package/dist/src/browser/screenshot.js.map +1 -0
  50. package/dist/src/browser/session.d.ts +8 -0
  51. package/dist/src/browser/session.d.ts.map +1 -0
  52. package/dist/src/browser/session.js +50 -0
  53. package/dist/src/browser/session.js.map +1 -0
  54. package/dist/src/browser/snapshot.d.ts +12 -0
  55. package/dist/src/browser/snapshot.d.ts.map +1 -0
  56. package/dist/src/browser/snapshot.js +147 -0
  57. package/dist/src/browser/snapshot.js.map +1 -0
  58. package/dist/src/browser/tabs.d.ts +8 -0
  59. package/dist/src/browser/tabs.d.ts.map +1 -0
  60. package/dist/src/browser/tabs.js +25 -0
  61. package/dist/src/browser/tabs.js.map +1 -0
  62. package/dist/src/browser/types.d.ts +109 -0
  63. package/dist/src/browser/types.d.ts.map +1 -0
  64. package/dist/src/browser/types.js +16 -0
  65. package/dist/src/browser/types.js.map +1 -0
  66. package/dist/src/browser/wait.d.ts +4 -0
  67. package/dist/src/browser/wait.d.ts.map +1 -0
  68. package/dist/src/browser/wait.js +122 -0
  69. package/dist/src/browser/wait.js.map +1 -0
  70. package/dist/src/commands/browse.d.ts +8 -0
  71. package/dist/src/commands/browse.d.ts.map +1 -0
  72. package/dist/src/commands/browse.js +573 -0
  73. package/dist/src/commands/browse.js.map +1 -0
  74. package/dist/src/commands/index.d.ts.map +1 -1
  75. package/dist/src/commands/index.js +2 -0
  76. package/dist/src/commands/index.js.map +1 -1
  77. package/dist/src/ui/dashboard-v2.html +1692 -0
  78. package/dist/src/ui/server.mjs +15 -1
  79. package/dist/tsconfig.tsbuildinfo +1 -1
  80. package/package.json +2 -1
@@ -11,7 +11,7 @@
11
11
  * node auto-memory-hook.mjs status # Show bridge status
12
12
  */
13
13
 
14
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
14
+ import { existsSync, mkdirSync, openSync, closeSync, unlinkSync, readFileSync, writeFileSync, renameSync } from 'fs';
15
15
  import { join, dirname } from 'path';
16
16
  import { fileURLToPath } from 'url';
17
17
 
@@ -34,6 +34,37 @@ const dim = (msg) => console.log(` ${DIM}${msg}${RESET}`);
34
34
  // Ensure data dir
35
35
  if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
36
36
 
37
+ // ============================================================================
38
+ // File-lock helper for concurrent hook coordination
39
+ // Uses O_EXCL exclusive-create as a spin lock.
40
+ // A 10ms synchronous wait (via Atomics.wait) is used between retries to avoid
41
+ // pegging the CPU. Acceptable for low-contention hook processes.
42
+ // ============================================================================
43
+
44
+ function withFileLock(lockPath, fn, timeoutMs = 5000) {
45
+ const start = Date.now();
46
+ const sharedBuf = new Int32Array(new SharedArrayBuffer(4));
47
+ while (true) {
48
+ try {
49
+ // O_EXCL: exclusive create — fails atomically if file already exists
50
+ const fd = openSync(lockPath, 'wx');
51
+ closeSync(fd);
52
+ break; // acquired lock
53
+ } catch {
54
+ if (Date.now() - start > timeoutMs) {
55
+ // Timeout: proceed without lock rather than deadlocking hook processes
56
+ try { fn(); } catch { /* best effort */ }
57
+ return;
58
+ }
59
+ // Synchronous 10ms sleep via Atomics.wait (avoids busy-spin CPU peg)
60
+ Atomics.wait(sharedBuf, 0, 0, 10);
61
+ }
62
+ }
63
+ try { fn(); } finally {
64
+ try { unlinkSync(lockPath); } catch { /* ignore */ }
65
+ }
66
+ }
67
+
37
68
  // ============================================================================
38
69
  // Simple JSON File Backend (implements IMemoryBackend interface)
39
70
  // ============================================================================
@@ -120,9 +151,13 @@ class JsonFileBackend {
120
151
  }
121
152
 
122
153
  _persist() {
123
- try {
124
- writeFileSync(this.filePath, JSON.stringify([...this.entries.values()], null, 2), 'utf-8');
125
- } catch { /* best effort */ }
154
+ const lockPath = this.filePath + '.lock';
155
+ withFileLock(lockPath, () => {
156
+ const data = JSON.stringify([...this.entries.values()], null, 2);
157
+ const tmpPath = this.filePath + '.tmp';
158
+ writeFileSync(tmpPath, data, 'utf-8');
159
+ renameSync(tmpPath, this.filePath);
160
+ });
126
161
  }
127
162
  }
128
163
 
@@ -0,0 +1,145 @@
1
+ 'use strict';
2
+ // Extracted from hook-handler.cjs — receives hCtx from dispatcher.
3
+ // Handles the 'post-edit' 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
+ handle: async function(hCtx) {
11
+ var hookInput = hCtx.hookInput;
12
+ var toolInput = hCtx.toolInput;
13
+ var args = hCtx.args;
14
+ var session = hCtx.session;
15
+ var intelligence = hCtx.intelligence;
16
+ var CWD = hCtx.CWD;
17
+
18
+ if (session && session.metric) {
19
+ try { session.metric('edits'); } catch (e) { /* no active session */ }
20
+ }
21
+ if (intelligence && intelligence.recordEdit) {
22
+ try {
23
+ var file = hookInput.file_path || toolInput.file_path
24
+ || process.env.TOOL_INPUT_file_path || args[0] || '';
25
+ intelligence.recordEdit(file);
26
+ } catch (e) { /* non-fatal */ }
27
+ }
28
+ // Track recently-edited files for compact injection and pre-resolve boosting.
29
+ try {
30
+ var editedForRecent = hookInput.file_path || toolInput.file_path
31
+ || process.env.TOOL_INPUT_file_path || args[0] || '';
32
+ if (editedForRecent) hCtx._recordRecentEdit(editedForRecent);
33
+ } catch (e) { /* non-fatal */ }
34
+ // Increment write counter and rebuild monograph when threshold hit.
35
+ hCtx._maybeRebuildMonograph();
36
+
37
+ // Test feedback (detection-only): when editing a source file, list tests
38
+ // that import it so the LLM/user knows what to verify next.
39
+ try {
40
+ var editedFile = hookInput.file_path || toolInput.file_path
41
+ || process.env.TOOL_INPUT_file_path || args[0] || '';
42
+ if (editedFile && !editedFile.match(/\.(test|spec)\./) && !editedFile.includes('__tests__')) {
43
+ var affectedTests = hCtx._findAffectedTests(editedFile);
44
+ if (affectedTests.length > 0) {
45
+ console.log('[AFFECTED_TESTS] ' + affectedTests.length + ' test(s) cover this file:');
46
+ for (var ti = 0; ti < Math.min(5, affectedTests.length); ti++) {
47
+ console.log(' · ' + affectedTests[ti]);
48
+ }
49
+ }
50
+ }
51
+ } catch (e) {}
52
+ // ── Security-Sensitive File Auto-Alert ────────────────────────────────────
53
+ // When editing auth, security, crypto, or env-related files, flag it
54
+ try {
55
+ var editFile = (hookInput.file_path || toolInput.file_path
56
+ || process.env.TOOL_INPUT_file_path || args[0] || '').toLowerCase();
57
+ var securityPatterns = /\b(auth|security|crypto|secret|credential|token|password|\.env|permission|acl|rbac|jwt|oauth|session|cookie)\b/;
58
+ if (securityPatterns.test(editFile) || editFile.includes('/security/') || editFile.includes('/auth/')) {
59
+ console.log('[SECURITY_EDIT] Security-sensitive file modified: ' + path.basename(editFile));
60
+ 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 + '"');
61
+ }
62
+ } catch (e) { /* non-fatal */ }
63
+
64
+ // ── Smart Test/Build Suggestions (PE-001) ───────────────────────────
65
+ try {
66
+ var editFile2 = (hookInput.file_path || toolInput.file_path
67
+ || process.env.TOOL_INPUT_file_path || args[0] || '');
68
+ var editBase = path.basename(editFile2).toLowerCase();
69
+ if (/\.(test|spec)\.(ts|js|tsx|jsx)$/.test(editBase)) {
70
+ console.log('[AUTO_SUGGEST] Test file modified — run: npm test -- --testPathPattern="' + path.basename(editFile2) + '"');
71
+ } else if (editBase === 'package.json') {
72
+ console.log('[AUTO_SUGGEST] package.json changed — consider running: npm install');
73
+ } else if (editBase === 'tsconfig.json' || editBase === 'tsconfig.base.json') {
74
+ console.log('[AUTO_SUGGEST] TypeScript config changed — consider running: npm run build');
75
+ }
76
+ } catch (e) { /* non-fatal */ }
77
+
78
+ // ── Monograph Incremental Rebuild ─────────────────────────────────────
79
+ // After every code file edit, trigger a background monograph rebuild so
80
+ // the knowledge graph stays current. Debounced via a lock file (5s cooldown).
81
+ try {
82
+ var editedFile2 = (hookInput.file_path || toolInput.file_path
83
+ || process.env.TOOL_INPUT_file_path || args[0] || '');
84
+ var codeExts = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|cs|cpp|c|rb|swift|php)$/i;
85
+ if (editedFile2 && codeExts.test(editedFile2)) {
86
+ var lockFile = path.join(CWD, '.monomind', 'graph', '.rebuild-lock');
87
+ var now = Date.now();
88
+ var lastBuild = 0;
89
+ try { lastBuild = parseInt(fs.readFileSync(lockFile, 'utf-8').trim(), 10) || 0; } catch (e) {}
90
+ var COOLDOWN_MS = 5000; // 5-second debounce
91
+ if (now - lastBuild > COOLDOWN_MS) {
92
+ fs.writeFileSync(lockFile, String(now), 'utf-8');
93
+ var { spawn: spawnRebuild } = require('child_process');
94
+ var rebuildScript = "import { buildAsync } from '@monoes/monograph'; await buildAsync(" + JSON.stringify(CWD) + ");";
95
+ var graphDir = path.join(CWD, '.monomind', 'graph');
96
+ var logPath = path.join(graphDir, 'build.log');
97
+ var logFd;
98
+ try { logFd = fs.openSync(logPath, 'a'); } catch(e) { logFd = 'ignore'; }
99
+ var child = spawnRebuild(process.execPath, ['--input-type=module', '--eval', rebuildScript], {
100
+ detached: true, stdio: ['ignore', logFd, logFd], cwd: CWD,
101
+ });
102
+ child.unref();
103
+ console.log('[MONOGRAPH] Incremental rebuild triggered for ' + path.basename(editedFile2));
104
+
105
+ // Option C: fire ua-enrich.mjs in background after monograph rebuild
106
+ var uaEnrichScript = path.join(CWD, 'scripts', 'ua-enrich.mjs');
107
+ if (fs.existsSync(uaEnrichScript)) {
108
+ var uaChild = spawnRebuild(process.execPath, [uaEnrichScript, '--dir', CWD, '--file', editedFile2, '--db', path.join(CWD, '.monomind', 'monograph.db')], {
109
+ detached: true, stdio: 'ignore', cwd: CWD,
110
+ });
111
+ uaChild.unref();
112
+ }
113
+ }
114
+ // Show importers of the edited file so Claude sees blast radius
115
+ try {
116
+ var mgDbPath4 = path.join(CWD, '.monomind', 'monograph.db');
117
+ if (fs.existsSync(mgDbPath4)) {
118
+ var mgMod4 = null;
119
+ var _requireMonograph4 = hCtx._requireMonograph;
120
+ mgMod4 = _requireMonograph4 ? _requireMonograph4() : null;
121
+ if (mgMod4 && mgMod4.openDb) {
122
+ var db4 = mgMod4.openDb(mgDbPath4);
123
+ try {
124
+ var editedBase4 = path.basename(editedFile2).replace(/\.[^.]+$/, '');
125
+ var editNode4 = db4.prepare("SELECT id, name, label FROM nodes WHERE file_path LIKE ? OR name = ? LIMIT 1")
126
+ .get('%' + path.sep + path.basename(editedFile2), editedBase4);
127
+ if (editNode4) {
128
+ var editImporters4 = db4.prepare(
129
+ 'SELECT n2.name FROM edges e JOIN nodes n2 ON n2.id = e.source_id WHERE e.target_id = ? LIMIT 8'
130
+ ).all(editNode4.id);
131
+ if (editImporters4.length > 0) {
132
+ console.log('[MONOGRAPH_IMPACT] ' + editNode4.name + ' (' + editNode4.label + ') is depended on by: ' +
133
+ editImporters4.map(function(i) { return i.name; }).join(', '));
134
+ }
135
+ }
136
+ } finally { if (mgMod4.closeDb) mgMod4.closeDb(db4); }
137
+ }
138
+ }
139
+ } catch(e) { /* non-fatal */ }
140
+ }
141
+ } catch (e) { /* non-fatal */ }
142
+
143
+ console.log('[OK] Edit recorded');
144
+ }
145
+ };
@@ -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
+ };