@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.
- package/.claude/agents/generated/churn-analyst.md +53 -0
- package/.claude/agents/generated/code-reviewer.md +55 -0
- package/.claude/agents/generated/code-validator.md +57 -0
- package/.claude/agents/generated/complexity-scanner.md +56 -0
- package/.claude/agents/generated/devbot-orchestrator.md +58 -0
- package/.claude/agents/generated/devbot-planner.md +63 -0
- package/.claude/agents/generated/impact-assessor.md +54 -0
- package/.claude/commands/mastermind/master.md +88 -24
- package/.claude/helpers/control-start.cjs +60 -1
- package/.claude/helpers/event-logger.cjs +43 -2
- package/.claude/helpers/handlers/capture-handler.cjs +336 -0
- package/.claude/helpers/handlers/route-handler.cjs +20 -13
- package/.claude/helpers/handlers/session-restore-handler.cjs +14 -8
- package/.claude/helpers/hook-handler.cjs +57 -1
- package/.claude/helpers/intelligence.cjs +129 -57
- package/.claude/helpers/memory-palace.cjs +461 -0
- package/.claude/helpers/memory.cjs +134 -15
- package/.claude/helpers/metrics-db.mjs +87 -0
- package/.claude/helpers/router.cjs +296 -41
- package/.claude/helpers/session.cjs +107 -32
- package/.claude/helpers/statusline.cjs +138 -2
- package/.claude/helpers/toggle-statusline.cjs +73 -0
- package/.claude/helpers/token-tracker.cjs +934 -0
- package/.claude/helpers/utils/monograph.cjs +39 -4
- package/.claude/helpers/utils/telemetry.cjs +3 -3
- package/.claude/skills/mastermind/createorg.md +227 -16
- package/.claude/skills/mastermind/idea.md +15 -3
- package/.claude/skills/mastermind/runorg.md +2 -1
- package/dist/src/commands/doctor.d.ts.map +1 -1
- package/dist/src/commands/doctor.js +96 -4
- package/dist/src/commands/doctor.js.map +1 -1
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/org.d.ts +4 -0
- package/dist/src/commands/org.d.ts.map +1 -0
- package/dist/src/commands/org.js +93 -0
- package/dist/src/commands/org.js.map +1 -0
- package/dist/src/mcp-tools/memory-tools.js +6 -6
- package/dist/src/mcp-tools/memory-tools.js.map +1 -1
- package/dist/src/mcp-tools/monograph-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/monograph-tools.js +329 -37
- package/dist/src/mcp-tools/monograph-tools.js.map +1 -1
- package/dist/src/mcp-tools/session-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/session-tools.js +9 -10
- package/dist/src/mcp-tools/session-tools.js.map +1 -1
- package/dist/src/mcp-tools/task-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/task-tools.js +7 -8
- package/dist/src/mcp-tools/task-tools.js.map +1 -1
- package/dist/src/mcp-tools/types.d.ts +1 -0
- package/dist/src/mcp-tools/types.d.ts.map +1 -1
- package/dist/src/mcp-tools/types.js +49 -0
- package/dist/src/mcp-tools/types.js.map +1 -1
- package/dist/src/services/worker-daemon.d.ts.map +1 -1
- package/dist/src/services/worker-daemon.js +295 -5
- package/dist/src/services/worker-daemon.js.map +1 -1
- package/dist/src/transfer/serialization/cfp.js +1 -1
- package/dist/src/transfer/serialization/cfp.js.map +1 -1
- package/dist/src/ui/dashboard.html +2235 -178
- package/dist/src/ui/orgs.html +1 -0
- package/dist/src/ui/server.mjs +532 -133
- package/dist/tsconfig.tsbuildinfo +1 -1
- 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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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('
|
|
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
|
-
}
|
|
295
|
-
|
|
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
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
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 */ }
|