@monoes/monomindcli 1.13.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 (39) 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 +11 -11
  13. package/.claude/helpers/hook-handler.cjs +17 -1
  14. package/.claude/helpers/session.cjs +20 -2
  15. package/.claude/skills/mastermind/createorg.md +227 -16
  16. package/.claude/skills/mastermind/idea.md +15 -3
  17. package/.claude/skills/mastermind/runorg.md +2 -1
  18. package/dist/src/commands/index.js +2 -0
  19. package/dist/src/commands/org.d.ts +4 -0
  20. package/dist/src/commands/org.d.ts.map +1 -0
  21. package/dist/src/commands/org.js +93 -0
  22. package/dist/src/commands/org.js.map +1 -0
  23. package/dist/src/mcp-tools/memory-tools.js +6 -6
  24. package/dist/src/mcp-tools/memory-tools.js.map +1 -1
  25. package/dist/src/mcp-tools/session-tools.d.ts.map +1 -1
  26. package/dist/src/mcp-tools/session-tools.js +9 -10
  27. package/dist/src/mcp-tools/session-tools.js.map +1 -1
  28. package/dist/src/mcp-tools/task-tools.d.ts.map +1 -1
  29. package/dist/src/mcp-tools/task-tools.js +7 -8
  30. package/dist/src/mcp-tools/task-tools.js.map +1 -1
  31. package/dist/src/mcp-tools/types.d.ts +1 -0
  32. package/dist/src/mcp-tools/types.d.ts.map +1 -1
  33. package/dist/src/mcp-tools/types.js +49 -0
  34. package/dist/src/mcp-tools/types.js.map +1 -1
  35. package/dist/src/ui/dashboard.html +1639 -249
  36. package/dist/src/ui/orgs.html +1 -0
  37. package/dist/src/ui/server.mjs +389 -132
  38. package/dist/tsconfig.tsbuildinfo +1 -1
  39. 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));
@@ -382,9 +382,10 @@ module.exports = {
382
382
  } catch (e) { /* ignore */ }
383
383
  }
384
384
  if (nodeCount > 100) {
385
- // 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
386
386
  // sees the answer inline instead of being told to call a tool.
387
- 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);
388
389
 
389
390
  // Boost recently-edited files to the top of pre-resolve suggestions.
390
391
  // Even when the FTS index hasn't caught up to the latest edits, the
@@ -414,25 +415,24 @@ module.exports = {
414
415
  }
415
416
  }
416
417
  if (editBoosts.length > 0) {
417
- suggestions = editBoosts.concat(suggestions).slice(0, 5);
418
+ suggestions = editBoosts.concat(suggestions).slice(0, 3);
418
419
  }
419
420
  }
420
421
  } catch (e) { /* non-fatal */ }
421
422
 
422
423
  if (suggestions.length > 0) {
423
- console.log('\n[MONOGRAPH] ' + nodeCount + ' nodes indexed. Top files for this task (pre-resolved from graph):');
424
- for (var si = 0; si < suggestions.length; si++) {
425
- 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) {
426
427
  var editTag = s._editBoost ? ' ✎' : '';
427
- // Include :line suffix when available so LLM can navigate directly
428
428
  var fileLoc = (s.file || '');
429
429
  if (fileLoc && s.startLine != null) fileLoc = fileLoc + ':' + s.startLine;
430
- console.log(' · ' + s.name + ' [' + s.label + '] — ' + fileLoc + (s.deg ? ' (deg ' + s.deg + ')' : '') + editTag);
431
- }
432
- console.log(' Use mcp__monomind__monograph_query / monograph_impact for deeper drill-down.');
430
+ return s.name + ' [' + s.label + '] — ' + fileLoc + editTag;
431
+ });
432
+ console.log('[MONOGRAPH] ' + nodeCount + ' nodes. Top files: ' + hintParts.join(' · '));
433
433
  hCtx._recordGraphTelemetry('preresolve_hit');
434
434
  } else {
435
- 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.');
436
436
  hCtx._recordGraphTelemetry('preresolve_miss');
437
437
  }
438
438
  }
@@ -99,17 +99,33 @@ const [,, command, ...args] = process.argv;
99
99
  // Read stdin — Claude Code sends hook data as JSON via stdin
100
100
  // Uses a timeout to prevent hanging when stdin is in an ambiguous state
101
101
  // (not TTY, not a proper pipe) which happens with Claude Code hook invocations.
102
+ // Hard cap at 1 MiB: a legitimate hook payload (tool name + input) is at most
103
+ // a few KB; anything larger is either a bug or an adversarial OOM attempt.
104
+ const MAX_STDIN_BYTES = 1 * 1024 * 1024; // 1 MiB
102
105
  async function readStdin() {
103
106
  if (process.stdin.isTTY) return '';
104
107
  return new Promise((resolve) => {
105
108
  let data = '';
109
+ let byteCount = 0;
110
+ let truncated = false;
106
111
  const timer = setTimeout(() => {
107
112
  process.stdin.removeAllListeners();
108
113
  process.stdin.pause();
109
114
  resolve(data);
110
115
  }, 500);
111
116
  process.stdin.setEncoding('utf8');
112
- process.stdin.on('data', (chunk) => { data += chunk; });
117
+ process.stdin.on('data', (chunk) => {
118
+ if (truncated) return;
119
+ byteCount += Buffer.byteLength(chunk, 'utf8');
120
+ if (byteCount > MAX_STDIN_BYTES) {
121
+ truncated = true;
122
+ process.stdin.pause();
123
+ clearTimeout(timer);
124
+ resolve(''); // discard oversized input to prevent OOM
125
+ return;
126
+ }
127
+ data += chunk;
128
+ });
113
129
  process.stdin.on('end', () => { clearTimeout(timer); resolve(data); });
114
130
  process.stdin.on('error', () => { clearTimeout(timer); resolve(data); });
115
131
  process.stdin.resume();
@@ -1,7 +1,8 @@
1
1
  'use strict';
2
2
  /**
3
3
  * Session state management for hook-handler.cjs
4
- * Persists session data to .monomind/sessions/current.json
4
+ * Persists session data to .git/monomind/sessions/current.json (branch-agnostic,
5
+ * shared across git worktrees). Falls back to .monomind/sessions/ if not in a git repo.
5
6
  *
6
7
  * API: start(), restore(), end(), status(), metric(key), update(patch)
7
8
  */
@@ -10,7 +11,24 @@ const path = require('path');
10
11
  const fs = require('fs');
11
12
 
12
13
  const CWD = process.env.CLAUDE_PROJECT_DIR || process.cwd();
13
- const SESSIONS_DIR = path.join(CWD, '.monomind', 'sessions');
14
+
15
+ function getMonoDir(workDir) {
16
+ try {
17
+ const gitEntry = path.join(workDir, '.git');
18
+ const st = fs.statSync(gitEntry);
19
+ if (st.isDirectory()) return path.join(gitEntry, 'monomind');
20
+ if (st.isFile()) {
21
+ const m = fs.readFileSync(gitEntry, 'utf8').match(/^gitdir:\s*(.+)/m);
22
+ if (m) {
23
+ const worktreeDir = path.resolve(workDir, m[1].trim());
24
+ return path.join(path.dirname(path.dirname(worktreeDir)), 'monomind');
25
+ }
26
+ }
27
+ } catch {}
28
+ return path.join(workDir, '.monomind');
29
+ }
30
+
31
+ const SESSIONS_DIR = path.join(getMonoDir(CWD), 'sessions');
14
32
  const CURRENT_FILE = path.join(SESSIONS_DIR, 'current.json');
15
33
 
16
34
  var KNOWN_METRICS = new Set(['edits', 'commands', 'tasks', 'errors']);