@monoes/monomindcli 1.12.0 → 1.14.0

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 (61) hide show
  1. package/.claude/agents/generated/churn-analyst.md +53 -0
  2. package/.claude/agents/generated/code-reviewer.md +55 -0
  3. package/.claude/agents/generated/code-validator.md +57 -0
  4. package/.claude/agents/generated/complexity-scanner.md +56 -0
  5. package/.claude/agents/generated/devbot-orchestrator.md +58 -0
  6. package/.claude/agents/generated/devbot-planner.md +63 -0
  7. package/.claude/agents/generated/impact-assessor.md +54 -0
  8. package/.claude/commands/mastermind/master.md +88 -24
  9. package/.claude/helpers/control-start.cjs +60 -1
  10. package/.claude/helpers/event-logger.cjs +43 -2
  11. package/.claude/helpers/handlers/capture-handler.cjs +336 -0
  12. package/.claude/helpers/handlers/route-handler.cjs +20 -13
  13. package/.claude/helpers/handlers/session-restore-handler.cjs +14 -8
  14. package/.claude/helpers/hook-handler.cjs +57 -1
  15. package/.claude/helpers/intelligence.cjs +129 -57
  16. package/.claude/helpers/memory-palace.cjs +461 -0
  17. package/.claude/helpers/memory.cjs +134 -15
  18. package/.claude/helpers/metrics-db.mjs +87 -0
  19. package/.claude/helpers/router.cjs +296 -41
  20. package/.claude/helpers/session.cjs +107 -32
  21. package/.claude/helpers/statusline.cjs +138 -2
  22. package/.claude/helpers/toggle-statusline.cjs +73 -0
  23. package/.claude/helpers/token-tracker.cjs +934 -0
  24. package/.claude/helpers/utils/monograph.cjs +39 -4
  25. package/.claude/helpers/utils/telemetry.cjs +3 -3
  26. package/.claude/skills/mastermind/createorg.md +227 -16
  27. package/.claude/skills/mastermind/idea.md +15 -3
  28. package/.claude/skills/mastermind/runorg.md +2 -1
  29. package/dist/src/commands/doctor.d.ts.map +1 -1
  30. package/dist/src/commands/doctor.js +96 -4
  31. package/dist/src/commands/doctor.js.map +1 -1
  32. package/dist/src/commands/index.js +2 -0
  33. package/dist/src/commands/org.d.ts +4 -0
  34. package/dist/src/commands/org.d.ts.map +1 -0
  35. package/dist/src/commands/org.js +93 -0
  36. package/dist/src/commands/org.js.map +1 -0
  37. package/dist/src/mcp-tools/memory-tools.js +6 -6
  38. package/dist/src/mcp-tools/memory-tools.js.map +1 -1
  39. package/dist/src/mcp-tools/monograph-tools.d.ts.map +1 -1
  40. package/dist/src/mcp-tools/monograph-tools.js +329 -37
  41. package/dist/src/mcp-tools/monograph-tools.js.map +1 -1
  42. package/dist/src/mcp-tools/session-tools.d.ts.map +1 -1
  43. package/dist/src/mcp-tools/session-tools.js +9 -10
  44. package/dist/src/mcp-tools/session-tools.js.map +1 -1
  45. package/dist/src/mcp-tools/task-tools.d.ts.map +1 -1
  46. package/dist/src/mcp-tools/task-tools.js +7 -8
  47. package/dist/src/mcp-tools/task-tools.js.map +1 -1
  48. package/dist/src/mcp-tools/types.d.ts +1 -0
  49. package/dist/src/mcp-tools/types.d.ts.map +1 -1
  50. package/dist/src/mcp-tools/types.js +49 -0
  51. package/dist/src/mcp-tools/types.js.map +1 -1
  52. package/dist/src/services/worker-daemon.d.ts.map +1 -1
  53. package/dist/src/services/worker-daemon.js +295 -5
  54. package/dist/src/services/worker-daemon.js.map +1 -1
  55. package/dist/src/transfer/serialization/cfp.js +1 -1
  56. package/dist/src/transfer/serialization/cfp.js.map +1 -1
  57. package/dist/src/ui/dashboard.html +2235 -178
  58. package/dist/src/ui/orgs.html +1 -0
  59. package/dist/src/ui/server.mjs +532 -133
  60. package/dist/tsconfig.tsbuildinfo +1 -1
  61. package/package.json +1 -1
@@ -61,6 +61,29 @@ function findCliPath() {
61
61
  const local = path.join(CWD, 'packages', '@monomind', 'cli', 'bin', 'cli.js');
62
62
  if (fs.existsSync(local)) return { cmd: process.execPath, args: [local], usePort: false };
63
63
 
64
+ // Try global npm install paths for both package names
65
+ // npm root -g is slow; probe known conventional paths instead
66
+ const globalCandidates = [];
67
+ try {
68
+ const { execSync } = require('child_process');
69
+ const npmRoot = execSync('npm root -g', { timeout: 3000, encoding: 'utf-8' }).trim();
70
+ if (npmRoot) {
71
+ globalCandidates.push(
72
+ path.join(npmRoot, 'monomind', 'packages', '@monomind', 'cli', 'dist', 'src', 'ui', 'server.mjs'),
73
+ path.join(npmRoot, '@monoes', 'monomindcli', 'dist', 'src', 'ui', 'server.mjs'),
74
+ path.join(npmRoot, 'monomind', 'packages', '@monomind', 'cli', 'bin', 'cli.js'),
75
+ path.join(npmRoot, '@monoes', 'monomindcli', 'bin', 'cli.js'),
76
+ );
77
+ }
78
+ } catch { /* npm root -g failed — skip */ }
79
+
80
+ for (const candidate of globalCandidates) {
81
+ if (fs.existsSync(candidate)) {
82
+ const usePort = candidate.endsWith('server.mjs');
83
+ return { cmd: process.execPath, args: [candidate], usePort };
84
+ }
85
+ }
86
+
64
87
  // Try npx monomind as last resort
65
88
  return { cmd: 'npx', args: ['monomind@latest'], usePort: false };
66
89
  }
@@ -88,9 +111,45 @@ function main() {
88
111
 
89
112
  child.unref();
90
113
 
114
+ // Write optimistic status with DEFAULT_PORT immediately so dependent scripts
115
+ // (hooks, boss agents) have something to read while the server starts up.
91
116
  writeStatus(child.pid, DEFAULT_PORT);
92
117
  process.stdout.write(`[control] started Neural Control Room on port ${DEFAULT_PORT} (pid ${child.pid})\n`);
93
- process.exit(0);
118
+
119
+ // If port 4242 was in use, server.mjs auto-increments (up to +10).
120
+ // Poll a few ports to find where it actually bound and update control.json.
121
+ const http = require('http');
122
+ function probePort(p) {
123
+ return new Promise((resolve) => {
124
+ const req = http.get({ hostname: 'localhost', port: p, path: '/api/status', timeout: 1000 }, (res) => {
125
+ res.resume();
126
+ resolve(res.statusCode < 500);
127
+ });
128
+ req.on('error', () => resolve(false));
129
+ req.on('timeout', () => { req.destroy(); resolve(false); });
130
+ });
131
+ }
132
+
133
+ // Give the server up to ~3 s to start, then confirm the actual port.
134
+ async function confirmPort() {
135
+ for (let attempt = 0; attempt < 6; attempt++) {
136
+ await new Promise(r => setTimeout(r, 500));
137
+ for (let delta = 0; delta <= 10; delta++) {
138
+ const p = DEFAULT_PORT + delta;
139
+ if (await probePort(p)) {
140
+ if (p !== DEFAULT_PORT) {
141
+ writeStatus(child.pid, p);
142
+ process.stdout.write(`[control] server bound to port ${p} (updated control.json)\n`);
143
+ }
144
+ return;
145
+ }
146
+ }
147
+ }
148
+ // Server didn't respond in time — leave control.json as-is (best-effort)
149
+ process.stdout.write('[control] server did not respond within 3 s — control.json may have wrong port\n');
150
+ }
151
+
152
+ confirmPort().catch(() => {}).finally(() => process.exit(0));
94
153
  }
95
154
 
96
155
  main();
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Universal Event Logger
4
- * Captures 100% of Claude Code hook events to .monomind/events/ JSONL files.
4
+ * Captures 100% of Claude Code hook events.
5
+ * Writes to .git/monomind/events/ (branch-agnostic, shared across worktrees).
6
+ * Falls back to .monomind/events/ when git is unavailable.
5
7
  * Runs as a fast, non-blocking hook — append-only, no processing.
6
8
  */
7
9
 
@@ -83,8 +85,26 @@ async function main() {
83
85
  // Remove undefined keys
84
86
  Object.keys(entry).forEach(k => entry[k] === undefined && delete entry[k]);
85
87
 
88
+ // Resolve the git-safe monomind root (mirrors _getGitMonomindDir in server.mjs)
89
+ function getMonoDir(workDir) {
90
+ try {
91
+ const gitEntry = path.join(workDir, '.git');
92
+ const st = fs.statSync(gitEntry);
93
+ if (st.isDirectory()) return path.join(gitEntry, 'monomind');
94
+ if (st.isFile()) {
95
+ const m = fs.readFileSync(gitEntry, 'utf8').match(/^gitdir:\s*(.+)/m);
96
+ if (m) {
97
+ const worktreeDir = path.resolve(workDir, m[1].trim());
98
+ return path.join(path.dirname(path.dirname(worktreeDir)), 'monomind');
99
+ }
100
+ }
101
+ } catch {}
102
+ return path.join(workDir, '.monomind');
103
+ }
104
+ const monoDir = getMonoDir(CWD);
105
+
86
106
  // Write to daily all-events log
87
- const eventsDir = path.join(CWD, '.monomind', 'events');
107
+ const eventsDir = path.join(monoDir, 'events');
88
108
  const today = new Date().toISOString().slice(0, 10);
89
109
  const allLog = path.join(eventsDir, `${today}-all-events.jsonl`);
90
110
 
@@ -101,6 +121,27 @@ async function main() {
101
121
  } catch {}
102
122
  }
103
123
 
124
+ // Solution 5: cross-reference Claude session ID into the active mastermind session.
125
+ // When a new Claude session starts, record its UUID in current.json so future
126
+ // lookups can join mastermind session state with the Claude conversation transcript.
127
+ if (eventType === 'session-start' && sessionId !== 'unknown') {
128
+ try {
129
+ const sessionsDir = path.join(monoDir, 'sessions');
130
+ const currentFile = path.join(sessionsDir, 'current.json');
131
+ if (fs.existsSync(currentFile)) {
132
+ const cur = JSON.parse(fs.readFileSync(currentFile, 'utf8'));
133
+ const claudeSessions = Array.isArray(cur.claude_sessions) ? cur.claude_sessions : [];
134
+ if (!claudeSessions.includes(sessionId)) {
135
+ claudeSessions.push(sessionId);
136
+ const updated = { ...cur, claude_sessions: claudeSessions };
137
+ const tmp = `${currentFile}.${process.pid}.tmp`;
138
+ fs.writeFileSync(tmp, JSON.stringify(updated, null, 2), 'utf8');
139
+ fs.renameSync(tmp, currentFile);
140
+ }
141
+ }
142
+ } catch {}
143
+ }
144
+
104
145
  // Forward notifications and subagent events to dashboard for live visibility
105
146
  const forwardTypes = ['notification', 'subagent-start', 'subagent-stop', 'user-prompt'];
106
147
  if (forwardTypes.includes(eventType)) {
@@ -0,0 +1,336 @@
1
+ 'use strict';
2
+ // Comprehensive session + subagent capture handler.
3
+ // Wired to SubagentStart (snapshot + spawn event) and SubagentStop (diff + emit).
4
+ //
5
+ // On SubagentStart:
6
+ // - Snapshots ~/.claude/projects/{proj}/*.jsonl file sizes
7
+ // - Emits agent:spawn to link the agent to the current mastermind session immediately
8
+ //
9
+ // On SubagentStop:
10
+ // - Diffs the snapshot, parses new JSONL files for token usage and last messages
11
+ // - Emits agent:complete (replaces org:comms) with full result and token data
12
+ // - Emits agent:usage for cost tracking
13
+ // - Persists to per-run capture log
14
+ //
15
+ // Active session awareness:
16
+ // - Reads .monomind/capture/active-session.json (written by server on session:start)
17
+ // - Reads .monomind/capture/active-run.json (written by server on run:start)
18
+ // - Includes sessionId in all emitted events so they link to the dashboard session
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const http = require('http');
23
+ const os = require('os');
24
+
25
+ const CWD = process.env.CLAUDE_PROJECT_DIR || process.cwd();
26
+ const CAPTURE_DIR = path.join(CWD, '.monomind', 'capture');
27
+
28
+ // ─── Helpers ────────────────────────────────────────────────────────────────
29
+
30
+ function readStdin() {
31
+ return new Promise((resolve) => {
32
+ let data = '';
33
+ process.stdin.on('data', c => data += c);
34
+ process.stdin.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve({}); } });
35
+ setTimeout(() => resolve({}), 3000);
36
+ });
37
+ }
38
+
39
+ function getClaudeProjectDir() {
40
+ const encoded = CWD.replace(/\//g, '-');
41
+ return path.join(os.homedir(), '.claude', 'projects', encoded);
42
+ }
43
+
44
+ function snapshotJSONLFiles() {
45
+ const claudeDir = getClaudeProjectDir();
46
+ if (!fs.existsSync(claudeDir)) return [];
47
+ try {
48
+ return fs.readdirSync(claudeDir)
49
+ .filter(f => f.endsWith('.jsonl'))
50
+ .map(f => {
51
+ try { return { name: f, size: fs.statSync(path.join(claudeDir, f)).size }; }
52
+ catch { return { name: f, size: 0 }; }
53
+ });
54
+ } catch { return []; }
55
+ }
56
+
57
+ function parseJSONLForData(filePath) {
58
+ let tin = 0, tout = 0;
59
+ let allMsgs = [];
60
+ let toolCalls = [];
61
+ try {
62
+ const lines = fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean);
63
+ for (const line of lines) {
64
+ try {
65
+ const d = JSON.parse(line);
66
+ const u = d?.message?.usage || {};
67
+ tin += (u.input_tokens || 0) + (u.cache_creation_input_tokens || 0);
68
+ tout += (u.output_tokens || 0);
69
+ if (d?.message?.role === 'assistant') {
70
+ const content = d?.message?.content;
71
+ if (Array.isArray(content)) {
72
+ const tb = content.find(b => b.type === 'text');
73
+ if (tb?.text) allMsgs.push(tb.text);
74
+ // Capture tool uses for context
75
+ const tools = content.filter(b => b.type === 'tool_use').map(b => b.name).filter(Boolean);
76
+ toolCalls.push(...tools);
77
+ } else if (typeof content === 'string' && content) {
78
+ allMsgs.push(content);
79
+ }
80
+ }
81
+ } catch { /* skip malformed lines */ }
82
+ }
83
+ } catch { /* unreadable */ }
84
+ // Return last 3 messages for context + first+last message
85
+ const lastMsg = allMsgs[allMsgs.length - 1] || '';
86
+ const firstMsg = allMsgs[0] || '';
87
+ const summary = allMsgs.length > 1
88
+ ? (firstMsg.slice(0, 300) + (allMsgs.length > 2 ? `\n…[${allMsgs.length - 2} more msgs]…\n` : '\n') + lastMsg.slice(0, 700))
89
+ : lastMsg.slice(0, 1000);
90
+ return { tokens_in: tin, tokens_out: tout, summary, last_msg: lastMsg, toolCalls: [...new Set(toolCalls)] };
91
+ }
92
+
93
+ function getActiveRun() {
94
+ const runFile = path.join(CAPTURE_DIR, 'active-run.json');
95
+ if (!fs.existsSync(runFile)) return null;
96
+ try {
97
+ const d = JSON.parse(fs.readFileSync(runFile, 'utf8'));
98
+ // Treat as stale after 8 hours (was 60 min — too short for long loops)
99
+ if (Date.now() - (d.ts || 0) > 8 * 60 * 60 * 1000) return null;
100
+ return d;
101
+ } catch { return null; }
102
+ }
103
+
104
+ function getActiveSession() {
105
+ const sessFile = path.join(CAPTURE_DIR, 'active-session.json');
106
+ if (!fs.existsSync(sessFile)) return null;
107
+ try {
108
+ const d = JSON.parse(fs.readFileSync(sessFile, 'utf8'));
109
+ // Treat as stale after 8 hours
110
+ if (Date.now() - (d.ts || 0) > 8 * 60 * 60 * 1000) return null;
111
+ return d; // { org, sessionId, ts }
112
+ } catch { return null; }
113
+ }
114
+
115
+ function postEvent(event) {
116
+ return new Promise((resolve) => {
117
+ try {
118
+ const ctrlPath = path.join(CWD, '.monomind', 'control.json');
119
+ let baseUrl = 'http://localhost:4242';
120
+ if (fs.existsSync(ctrlPath)) {
121
+ try { baseUrl = JSON.parse(fs.readFileSync(ctrlPath, 'utf8')).url || baseUrl; } catch {}
122
+ }
123
+ const u = new URL(baseUrl);
124
+ const body = JSON.stringify(event);
125
+ const req = http.request({
126
+ hostname: u.hostname,
127
+ port: parseInt(u.port || '4242', 10),
128
+ path: '/api/mastermind/event',
129
+ method: 'POST',
130
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
131
+ }, () => resolve());
132
+ req.on('error', () => resolve());
133
+ req.setTimeout(3000, () => { req.destroy(); resolve(); });
134
+ req.write(body);
135
+ req.end();
136
+ } catch { resolve(); }
137
+ });
138
+ }
139
+
140
+ // ─── SubagentStart: snapshot + emit agent:spawn ──────────────────────────────
141
+
142
+ async function handleSubagentStart(hookInput) {
143
+ fs.mkdirSync(CAPTURE_DIR, { recursive: true });
144
+
145
+ const snapshot = snapshotJSONLFiles();
146
+ const agentType = String(
147
+ hookInput.subagent_type || hookInput.agentType || hookInput.agent_type || 'unknown'
148
+ ).slice(0, 64).replace(/[^a-z0-9_-]/gi, '-');
149
+ const agentDesc = String(hookInput.description || hookInput.prompt_description || '').slice(0, 1000);
150
+ const activeRun = getActiveRun();
151
+ const activeSess = getActiveSession();
152
+
153
+ // Write snapshot to FIFO queue — subagent-stop pops the oldest
154
+ const snapFile = path.join(
155
+ CAPTURE_DIR,
156
+ 'snap-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6) + '.json'
157
+ );
158
+ fs.writeFileSync(snapFile, JSON.stringify({
159
+ ts: Date.now(),
160
+ files: snapshot,
161
+ agentType,
162
+ agentDesc,
163
+ org: activeSess?.org || activeRun?.org || null,
164
+ runId: activeRun?.runId || null,
165
+ session: activeSess?.sessionId || null,
166
+ }));
167
+
168
+ console.log('[CAPTURE:start] ' + agentType + ' · snapped ' + snapshot.length + ' files'
169
+ + (activeSess ? ' · sess=' + activeSess.sessionId : '')
170
+ + (activeRun ? ' · run=' + activeRun.runId : ' · no active run'));
171
+
172
+ // Emit agent:spawn immediately so the dashboard shows the agent starting
173
+ const org = activeSess?.org || activeRun?.org;
174
+ if (org || activeSess?.sessionId) {
175
+ await postEvent({
176
+ type: 'agent:spawn',
177
+ org: org || '',
178
+ runId: activeRun?.runId || '',
179
+ session: activeSess?.sessionId || '',
180
+ agentType,
181
+ task: agentDesc.slice(0, 500),
182
+ from: 'orchestrator',
183
+ to: agentType,
184
+ ts: Date.now(),
185
+ });
186
+ }
187
+ }
188
+
189
+ // ─── SubagentStop: diff + emit ────────────────────────────────────────────────
190
+
191
+ async function handleSubagentStop(hookInput) {
192
+ fs.mkdirSync(CAPTURE_DIR, { recursive: true });
193
+
194
+ // Pop oldest snapshot (FIFO — matches the agent that just finished)
195
+ const snapFiles = fs.readdirSync(CAPTURE_DIR)
196
+ .filter(f => f.startsWith('snap-') && f.endsWith('.json'))
197
+ .sort(); // lexicographic = timestamp order
198
+
199
+ if (snapFiles.length === 0) {
200
+ console.log('[CAPTURE:stop] No snapshot to diff — skipping');
201
+ return;
202
+ }
203
+
204
+ const snapPath = path.join(CAPTURE_DIR, snapFiles[0]);
205
+ let snap;
206
+ try { snap = JSON.parse(fs.readFileSync(snapPath, 'utf8')); } catch {
207
+ try { fs.unlinkSync(snapPath); } catch {}
208
+ return;
209
+ }
210
+ try { fs.unlinkSync(snapPath); } catch {}
211
+
212
+ // Also check current active session (may have changed since SubagentStart)
213
+ const activeSess = getActiveSession();
214
+ const session = snap.session || activeSess?.sessionId || null;
215
+ const org = snap.org || activeSess?.org || null;
216
+ const runId = snap.runId || null;
217
+
218
+ // Diff JSONL files — only count entirely NEW files (subagent creates its own session)
219
+ const claudeDir = getClaudeProjectDir();
220
+ const currentFiles = snapshotJSONLFiles();
221
+ const prevNames = new Set((snap.files || []).map(f => f.name));
222
+
223
+ let totalTin = 0, totalTout = 0;
224
+ let summary = '';
225
+ let toolCalls = [];
226
+ const capturedFiles = [];
227
+
228
+ for (const f of currentFiles) {
229
+ if (!prevNames.has(f.name)) {
230
+ const parsed = parseJSONLForData(path.join(claudeDir, f.name));
231
+ totalTin += parsed.tokens_in;
232
+ totalTout += parsed.tokens_out;
233
+ if (parsed.summary) summary = parsed.summary;
234
+ toolCalls.push(...parsed.toolCalls);
235
+ capturedFiles.push(f.name);
236
+ }
237
+ }
238
+
239
+ const costUsd = parseFloat((totalTin * 3e-6 + totalTout * 15e-6).toFixed(6));
240
+ const { agentType, agentDesc } = snap;
241
+ toolCalls = [...new Set(toolCalls)].slice(0, 20);
242
+
243
+ console.log('[CAPTURE:stop] ' + agentType + ' · ' + totalTin + '+' + totalTout
244
+ + ' tok · $' + costUsd.toFixed(4) + ' · ' + capturedFiles.length + ' new files'
245
+ + (session ? ' · sess=' + session : '')
246
+ + (org ? ' · ' + org + '/' + runId : ''));
247
+
248
+ if (!org && !session) {
249
+ // No active org or session — log to general capture file only
250
+ const genLog = path.join(CAPTURE_DIR, 'unattributed.jsonl');
251
+ fs.appendFileSync(genLog, JSON.stringify({
252
+ ts: Date.now(), agentType, tokens_in: totalTin, tokens_out: totalTout,
253
+ cost_usd: costUsd, capturedFiles, summary: summary.slice(0, 200),
254
+ }) + '\n');
255
+ return;
256
+ }
257
+
258
+ // ── Emit agent:usage (token/cost accounting) ──────────────────────────────
259
+ if (totalTin > 0 || totalTout > 0) {
260
+ await postEvent({
261
+ type: 'agent:usage',
262
+ org: org || '',
263
+ runId: runId || '',
264
+ session: session || '',
265
+ role: agentType,
266
+ agentType,
267
+ tokens_in: totalTin,
268
+ tokens_out: totalTout,
269
+ cost_usd: costUsd,
270
+ ts: Date.now(),
271
+ });
272
+ }
273
+
274
+ // ── Emit agent:complete (replaces org:comms — richer, includes session) ───
275
+ // Always emit even if summary is empty (so the spawn/complete pair is always visible)
276
+ await postEvent({
277
+ type: 'agent:complete',
278
+ org: org || '',
279
+ runId: runId || '',
280
+ session: session || '',
281
+ agentType,
282
+ role: agentType,
283
+ from: agentType,
284
+ to: 'boss',
285
+ result: summary.slice(0, 1000),
286
+ toolCalls,
287
+ tokens_in: totalTin,
288
+ tokens_out: totalTout,
289
+ cost_usd: costUsd,
290
+ capturedFiles: capturedFiles.slice(0, 10),
291
+ ts: Date.now(),
292
+ });
293
+
294
+ // ── Also emit legacy org:comms for backwards compat ───────────────────────
295
+ if (summary && (org || session)) {
296
+ await postEvent({
297
+ type: 'org:comms',
298
+ org: org || '',
299
+ runId: runId || '',
300
+ session: session || '',
301
+ from: agentType,
302
+ to: 'boss',
303
+ msg: summary.slice(0, 500),
304
+ ts: Date.now(),
305
+ });
306
+ }
307
+
308
+ // ── Persist transcript reference to run directory ─────────────────────────
309
+ if (capturedFiles.length > 0 && org && runId) {
310
+ const runDir = path.join(CWD, '.monomind', 'orgs', org, 'runs');
311
+ fs.mkdirSync(runDir, { recursive: true });
312
+ const capLog = path.join(runDir, runId + '-captures.jsonl');
313
+ fs.appendFileSync(capLog, JSON.stringify({
314
+ type: 'agent:capture',
315
+ org, runId, session,
316
+ agentType,
317
+ agentDesc: agentDesc.slice(0, 200),
318
+ tokens_in: totalTin,
319
+ tokens_out: totalTout,
320
+ cost_usd: costUsd,
321
+ capturedFiles,
322
+ claudeProjectDir: claudeDir,
323
+ ts: Date.now(),
324
+ }) + '\n');
325
+ }
326
+ }
327
+
328
+ // ─── Dispatch ─────────────────────────────────────────────────────────────────
329
+
330
+ const eventType = process.argv[2]; // 'subagent-start' | 'subagent-stop'
331
+
332
+ readStdin().then(hookInput => {
333
+ if (eventType === 'subagent-start') return handleSubagentStart(hookInput);
334
+ if (eventType === 'subagent-stop') return handleSubagentStop(hookInput);
335
+ console.log('[CAPTURE] unknown event type: ' + eventType);
336
+ }).catch(() => process.exit(0));
@@ -118,8 +118,8 @@ module.exports = {
118
118
  } catch (e) {}
119
119
 
120
120
  var output = [];
121
- output.push('[INFO] Routing task: ' + (prompt.substring(0, 80) || '(no prompt)'));
122
- output.push('');
121
+ // Skip the noisy "[INFO] Routing task: ..." prefix it repeats the user's
122
+ // own words back and adds token overhead without helping Claude.
123
123
  // Routing panel strategy:
124
124
  // conf >= 0.90 → show primary recommendation (router is confident, trust it)
125
125
  // conf < 0.90 → show category picker so Claude uses its own context to
@@ -185,13 +185,17 @@ module.exports = {
185
185
  var devAgentsForPersist = /^(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|Software Architect|Frontend Developer|AI Engineer|Data Engineer|Security Engineer|DevOps Automator|SRE)$/i;
186
186
  var persistedIsNonDev = !devAgentsForPersist.test(String(result.agent || '').trim());
187
187
  var resolvedAgent = result.agent;
188
+ var resolvedFromExtras = false;
188
189
  if (!resolvedAgent || resolvedAgent === 'extras') {
189
190
  var topExtra = result.extrasMatches && result.extrasMatches[0];
190
191
  resolvedAgent = topExtra ? topExtra.name : 'Specialist Agent';
192
+ resolvedFromExtras = true;
191
193
  }
192
194
  // If router was uncertain (< 90%) and picked a non-dev specialist,
193
195
  // show "AI selecting" in statusline rather than the wrong agent.
194
- if (confForPersist < 0.90 && persistedIsNonDev && !String(result.reason || '').startsWith('Graph fallback')) {
196
+ // Exception: when agent was explicitly 'extras' and we resolved from extrasMatches,
197
+ // trust that resolution — the domain specialist was explicitly matched.
198
+ if (!resolvedFromExtras && confForPersist < 0.90 && persistedIsNonDev && !String(result.reason || '').startsWith('Graph fallback')) {
195
199
  resolvedAgent = 'AI selecting';
196
200
  }
197
201
  var routePayload = {
@@ -378,9 +382,10 @@ module.exports = {
378
382
  } catch (e) { /* ignore */ }
379
383
  }
380
384
  if (nodeCount > 100) {
381
- // Pre-resolve top-5 relevant files for the user's prompt — the LLM
385
+ // Pre-resolve top-3 relevant files for the user's prompt — the LLM
382
386
  // sees the answer inline instead of being told to call a tool.
383
- var suggestions = hCtx.getMonographSuggestions(prompt, 5);
387
+ // 3 is enough signal; more files inflate token cost on every prompt.
388
+ var suggestions = hCtx.getMonographSuggestions(prompt, 3);
384
389
 
385
390
  // Boost recently-edited files to the top of pre-resolve suggestions.
386
391
  // Even when the FTS index hasn't caught up to the latest edits, the
@@ -410,22 +415,24 @@ module.exports = {
410
415
  }
411
416
  }
412
417
  if (editBoosts.length > 0) {
413
- suggestions = editBoosts.concat(suggestions).slice(0, 5);
418
+ suggestions = editBoosts.concat(suggestions).slice(0, 3);
414
419
  }
415
420
  }
416
421
  } catch (e) { /* non-fatal */ }
417
422
 
418
423
  if (suggestions.length > 0) {
419
- console.log('\n[MONOGRAPH] ' + nodeCount + ' nodes indexed. Top files for this task (pre-resolved from graph):');
420
- for (var si = 0; si < suggestions.length; si++) {
421
- var s = suggestions[si];
424
+ // Compact single-line format: "[MONOGRAPH] N nodes. Top files: name [Label] path:line, ..."
425
+ // Matches the agent-start-handler pattern keeps per-prompt token cost minimal.
426
+ var hintParts = suggestions.map(function(s) {
422
427
  var editTag = s._editBoost ? ' ✎' : '';
423
- console.log(' · ' + s.name + ' [' + s.label + '] — ' + (s.file || '') + (s.deg ? ' (deg ' + s.deg + ')' : '') + editTag);
424
- }
425
- console.log(' Use mcp__monomind__monograph_query / monograph_impact for deeper drill-down.');
428
+ var fileLoc = (s.file || '');
429
+ if (fileLoc && s.startLine != null) fileLoc = fileLoc + ':' + s.startLine;
430
+ return s.name + ' [' + s.label + '] ' + fileLoc + editTag;
431
+ });
432
+ console.log('[MONOGRAPH] ' + nodeCount + ' nodes. Top files: ' + hintParts.join(' · '));
426
433
  hCtx._recordGraphTelemetry('preresolve_hit');
427
434
  } else {
428
- console.log('\n[MONOGRAPH] ' + nodeCount + ' nodes indexed. Call mcp__monomind__monograph_suggest first to find relevant files without grepping.');
435
+ console.log('[MONOGRAPH] ' + nodeCount + ' nodes. Call mcp__monomind__monograph_suggest to find relevant files.');
429
436
  hCtx._recordGraphTelemetry('preresolve_miss');
430
437
  }
431
438
  }
@@ -291,8 +291,11 @@ module.exports = {
291
291
  daemonChild.on('error', function() {});
292
292
  daemonChild.unref();
293
293
  console.log('[DAEMON_AUTOSTART] Background daemon started (pid ' + daemonChild.pid + ')');
294
- } else {
295
- 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');
294
+ }
295
+ // Daemon not running + no autoStart: emit only if this project has a config
296
+ // (i.e. daemon was intentionally set up). Avoids noisy output in daemon-less projects.
297
+ else if (fs.existsSync(path.join(CWD, 'monomind.config.json'))) {
298
+ console.log('[DAEMON_STOPPED] Run `npx monomind daemon start` to enable background workers');
296
299
  }
297
300
  }
298
301
  } catch (e) { /* non-fatal */ }
@@ -328,11 +331,13 @@ module.exports = {
328
331
  }
329
332
  } catch (e) { /* non-fatal */ }
330
333
 
331
- // Monomind Control UI Status — only probe when a daemon has previously run
332
- // (indicated by daemon.pid or monomind.config.json). Skips silently in fresh
333
- // environments and test fixtures that have no daemon history.
334
- var _controlUiShouldProbe = fs.existsSync(path.join(CWD, '.monomind', 'daemon.pid'))
335
- || fs.existsSync(path.join(CWD, 'monomind.config.json'));
334
+ // Monomind Control UI Status — only probe when a daemon.pid file exists,
335
+ // meaning the daemon was intentionally started in this project. This avoids
336
+ // printing "[CONTROL_UI] offline" noise on every session in projects that
337
+ // never run the daemon. The broader "monomind.config.json" check is dropped
338
+ // because that file is present in the dev repo itself and in any initialized
339
+ // project, making the old condition nearly always true.
340
+ var _controlUiShouldProbe = fs.existsSync(path.join(CWD, '.monomind', 'daemon.pid'));
336
341
  if (_controlUiShouldProbe) {
337
342
  try {
338
343
  var http = require('http');
@@ -344,7 +349,8 @@ module.exports = {
344
349
  res.resume();
345
350
  });
346
351
  req.on('error', function() {
347
- console.log('[CONTROL_UI] offline run: npx monomind mcp start');
352
+ // Only warn when daemon was previously running (pid file exists but server is gone)
353
+ console.log('[CONTROL_UI] offline — restart with: npx monomind mcp start');
348
354
  });
349
355
  req.setTimeout(800, function() { req.destroy(); });
350
356
  } catch (e) { /* non-fatal */ }