monomind 2.0.3 → 2.1.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/package.json +9 -5
- package/packages/@monomind/cli/.claude/helpers/control-start.cjs +13 -20
- package/packages/@monomind/cli/.claude/helpers/event-logger.cjs +109 -6
- package/packages/@monomind/cli/.claude/helpers/graphify-freshen.cjs +19 -17
- package/packages/@monomind/cli/.claude/helpers/handlers/agent-start-handler.cjs +60 -28
- package/packages/@monomind/cli/.claude/helpers/handlers/capture-handler.cjs +75 -18
- package/packages/@monomind/cli/.claude/helpers/handlers/edit-handler.cjs +27 -9
- package/packages/@monomind/cli/.claude/helpers/handlers/gates-handler.cjs +16 -7
- package/packages/@monomind/cli/.claude/helpers/handlers/loops-status-handler.cjs +1 -1
- package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +4 -4
- package/packages/@monomind/cli/.claude/helpers/handlers/session-handler.cjs +40 -22
- package/packages/@monomind/cli/.claude/helpers/handlers/session-restore-handler.cjs +0 -12
- package/packages/@monomind/cli/.claude/helpers/handlers/task-handler.cjs +53 -22
- package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +54 -4
- package/packages/@monomind/cli/.claude/helpers/intelligence.cjs +94 -15
- package/packages/@monomind/cli/.claude/helpers/session.cjs +6 -1
- package/packages/@monomind/cli/.claude/helpers/statusline.cjs +10 -10
- package/packages/@monomind/cli/.claude/helpers/token-tracker.cjs +3 -3
- package/packages/@monomind/cli/.claude/helpers/utils/agent-registrations.cjs +41 -0
- package/packages/@monomind/cli/.claude/helpers/utils/fs-helpers.cjs +183 -0
- package/packages/@monomind/cli/.claude/helpers/utils/micro-agents.cjs +10 -6
- package/packages/@monomind/cli/.claude/helpers/utils/telemetry.cjs +35 -19
- package/packages/@monomind/cli/bin/cli.js +10 -1
- package/packages/@monomind/cli/dist/src/commands/agent-ops.js +9 -15
- package/packages/@monomind/cli/dist/src/commands/cleanup.js +20 -4
- package/packages/@monomind/cli/dist/src/commands/completions.js +66 -261
- package/packages/@monomind/cli/dist/src/commands/doctor.js +7 -2
- package/packages/@monomind/cli/dist/src/commands/hooks-workers.js +7 -0
- package/packages/@monomind/cli/dist/src/commands/index.js +3 -0
- package/packages/@monomind/cli/dist/src/commands/mcp.js +11 -6
- package/packages/@monomind/cli/dist/src/commands/memory-admin.js +12 -6
- package/packages/@monomind/cli/dist/src/commands/memory-transfer.js +6 -13
- package/packages/@monomind/cli/dist/src/commands/monograph.js +3 -3
- package/packages/@monomind/cli/dist/src/commands/org.d.ts +3 -1
- package/packages/@monomind/cli/dist/src/commands/org.js +306 -129
- package/packages/@monomind/cli/dist/src/commands/start.js +142 -37
- package/packages/@monomind/cli/dist/src/commands/tokens.js +21 -3
- package/packages/@monomind/cli/dist/src/config-adapter.js +37 -17
- package/packages/@monomind/cli/dist/src/index.js +64 -28
- package/packages/@monomind/cli/dist/src/init/claudemd-generator.js +44 -11
- package/packages/@monomind/cli/dist/src/init/executor.js +63 -49
- package/packages/@monomind/cli/dist/src/mcp-client.d.ts +8 -3
- package/packages/@monomind/cli/dist/src/mcp-client.js +37 -3
- package/packages/@monomind/cli/dist/src/mcp-server.js +8 -2
- package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.d.ts +20 -0
- package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.js +10 -5
- package/packages/@monomind/cli/dist/src/mcp-tools/github-tools.js +30 -8
- package/packages/@monomind/cli/dist/src/mcp-tools/graphify-tools.js +11 -2
- package/packages/@monomind/cli/dist/src/mcp-tools/hive-mind-tools.js +51 -34
- package/packages/@monomind/cli/dist/src/mcp-tools/knowledge-tools.js +9 -1
- package/packages/@monomind/cli/dist/src/mcp-tools/monograph-compat.js +56 -14
- package/packages/@monomind/cli/dist/src/mcp-tools/monograph-tools.js +65 -43
- package/packages/@monomind/cli/dist/src/mcp-tools/swarm-tools.js +6 -4
- package/packages/@monomind/cli/dist/src/mcp-tools/system-tools.js +4 -4
- package/packages/@monomind/cli/dist/src/mcp-tools/types.d.ts +20 -0
- package/packages/@monomind/cli/dist/src/mcp-tools/types.js +36 -1
- package/packages/@monomind/cli/dist/src/memory/hnsw-operations.d.ts +13 -1
- package/packages/@monomind/cli/dist/src/memory/hnsw-operations.js +66 -14
- package/packages/@monomind/cli/dist/src/memory/intelligence.js +99 -3
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.d.ts +7 -0
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +20 -0
- package/packages/@monomind/cli/dist/src/orgrt/broker.d.ts +24 -0
- package/packages/@monomind/cli/dist/src/orgrt/broker.js +68 -0
- package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +30 -0
- package/packages/@monomind/cli/dist/src/orgrt/daemon.js +96 -7
- package/packages/@monomind/cli/dist/src/orgrt/forwarder.d.ts +10 -5
- package/packages/@monomind/cli/dist/src/orgrt/forwarder.js +105 -8
- package/packages/@monomind/cli/dist/src/orgrt/live.html +56 -0
- package/packages/@monomind/cli/dist/src/orgrt/provider.js +1 -0
- package/packages/@monomind/cli/dist/src/orgrt/scheduler.d.ts +13 -0
- package/packages/@monomind/cli/dist/src/orgrt/scheduler.js +48 -0
- package/packages/@monomind/cli/dist/src/orgrt/server.d.ts +8 -0
- package/packages/@monomind/cli/dist/src/orgrt/server.js +72 -0
- package/packages/@monomind/cli/dist/src/orgrt/test-loop.d.ts +12 -0
- package/packages/@monomind/cli/dist/src/orgrt/test-loop.js +108 -0
- package/packages/@monomind/cli/dist/src/output.js +12 -5
- package/packages/@monomind/cli/dist/src/parser.d.ts +32 -0
- package/packages/@monomind/cli/dist/src/parser.js +130 -5
- package/packages/@monomind/cli/dist/src/routing/embed-worker.js +13 -1
- package/packages/@monomind/cli/dist/src/routing/route-layer-factory.js +31 -0
- package/packages/@monomind/cli/dist/src/services/config-file-manager.d.ts +21 -0
- package/packages/@monomind/cli/dist/src/services/config-file-manager.js +60 -7
- package/packages/@monomind/cli/dist/src/services/crash-reporter.js +2 -1
- package/packages/@monomind/cli/dist/src/ui/dashboard.html +40 -40
- package/packages/@monomind/cli/dist/src/ui/orgs.html +110 -11
- package/packages/@monomind/cli/dist/src/ui/server.mjs +304 -133
- package/packages/@monomind/cli/dist/src/utils/input-guards.d.ts +9 -0
- package/packages/@monomind/cli/dist/src/utils/input-guards.js +39 -5
- package/packages/@monomind/cli/package.json +6 -2
|
@@ -351,7 +351,7 @@ function parseSessionFile(filePath, project, seenMsgIds, dateStart, dateEnd) {
|
|
|
351
351
|
function collectJsonlFiles(dirPath) {
|
|
352
352
|
var files = [];
|
|
353
353
|
var entries;
|
|
354
|
-
try { entries = fs.readdirSync(dirPath); } catch (e) { return files; }
|
|
354
|
+
try { entries = fs.readdirSync(dirPath).filter(function(f) { return !f.startsWith('._'); }); } catch (e) { return files; }
|
|
355
355
|
for (var i = 0; i < entries.length; i++) {
|
|
356
356
|
var e = entries[i];
|
|
357
357
|
if (e.endsWith('.jsonl')) {
|
|
@@ -360,7 +360,7 @@ function collectJsonlFiles(dirPath) {
|
|
|
360
360
|
// Check subagents
|
|
361
361
|
var subDir = path.join(dirPath, e, 'subagents');
|
|
362
362
|
var subFiles;
|
|
363
|
-
try { subFiles = fs.readdirSync(subDir); } catch (_) { continue; }
|
|
363
|
+
try { subFiles = fs.readdirSync(subDir).filter(function(f) { return !f.startsWith('._'); }); } catch (_) { continue; }
|
|
364
364
|
for (var j = 0; j < subFiles.length; j++) {
|
|
365
365
|
if (subFiles[j].endsWith('.jsonl')) {
|
|
366
366
|
files.push(path.join(subDir, subFiles[j]));
|
|
@@ -389,7 +389,7 @@ function unsanitize(name) {
|
|
|
389
389
|
function parseAllSessions(dateStart, dateEnd) {
|
|
390
390
|
var projectsDir = getClaudeProjectsDir();
|
|
391
391
|
var projectDirs;
|
|
392
|
-
try { projectDirs = fs.readdirSync(projectsDir); } catch (e) { return []; }
|
|
392
|
+
try { projectDirs = fs.readdirSync(projectsDir).filter(function(f) { return !f.startsWith('._'); }); } catch (e) { return []; }
|
|
393
393
|
|
|
394
394
|
var seenMsgIds = new Set();
|
|
395
395
|
var projectMap = {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Shared agent-registration purge logic.
|
|
3
|
+
//
|
|
4
|
+
// .monomind/agents/registrations/*.json accumulate one file per subagent
|
|
5
|
+
// spawn. The only purge used to be the FIFO-oldest-removal + stale sweep in
|
|
6
|
+
// task-handler.cjs's handlePostTask, which only fires on TeammateIdle/
|
|
7
|
+
// TaskCompleted — so any session that spawns agents but never emits those
|
|
8
|
+
// events (crashes, single-shot Task calls outside a team, etc.) leaked
|
|
9
|
+
// registrations forever. agent-start-handler.cjs already reads this
|
|
10
|
+
// directory on every subagent start, so it also runs this same stale sweep.
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const { cleanEntries } = require('./fs-helpers.cjs');
|
|
15
|
+
|
|
16
|
+
const DEFAULT_MAX_AGE_MS = 30 * 60 * 1000; // 30 minutes
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Delete registration files older than maxAgeMs. Returns the number of
|
|
20
|
+
* registrations remaining after the purge (or null if regDir doesn't exist
|
|
21
|
+
* or an error occurred).
|
|
22
|
+
*/
|
|
23
|
+
function purgeStaleRegistrations(regDir, maxAgeMs) {
|
|
24
|
+
maxAgeMs = maxAgeMs || DEFAULT_MAX_AGE_MS;
|
|
25
|
+
try {
|
|
26
|
+
if (!fs.existsSync(regDir)) return null;
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
for (const f of cleanEntries(regDir, f => f.endsWith('.json'))) {
|
|
29
|
+
try {
|
|
30
|
+
if (now - fs.statSync(path.join(regDir, f)).mtimeMs > maxAgeMs) {
|
|
31
|
+
fs.unlinkSync(path.join(regDir, f));
|
|
32
|
+
}
|
|
33
|
+
} catch { /* ignore */ }
|
|
34
|
+
}
|
|
35
|
+
return cleanEntries(regDir, f => f.endsWith('.json')).length;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { purgeStaleRegistrations, DEFAULT_MAX_AGE_MS };
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Shared fs helpers for hook handlers.
|
|
3
|
+
//
|
|
4
|
+
// exFAT / macOS AppleDouble junk files (`._foo.json`, `.__protocol.md`, etc.)
|
|
5
|
+
// get created whenever files touch an exFAT volume (external drives, some
|
|
6
|
+
// network shares) and then get picked up by naive `readdirSync().filter(...)`
|
|
7
|
+
// calls as if they were real data — corrupting counts, getting parsed as
|
|
8
|
+
// JSON and failing, or (worst case) surfacing as garbage entries in
|
|
9
|
+
// Claude Code's own skill/command list. Every readdir in the hook handlers
|
|
10
|
+
// should route through `cleanEntries()` so this is filtered exactly once.
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Read a directory and filter out exFAT AppleDouble junk (`._*`) before
|
|
17
|
+
* applying the caller's own filter predicate.
|
|
18
|
+
* @param {string} dir - directory to read
|
|
19
|
+
* @param {(name: string) => boolean} [filterFn] - optional additional filter
|
|
20
|
+
* @returns {string[]} entry names (not full paths), junk-free
|
|
21
|
+
*/
|
|
22
|
+
function cleanEntries(dir, filterFn) {
|
|
23
|
+
let entries;
|
|
24
|
+
try {
|
|
25
|
+
entries = fs.readdirSync(dir);
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
entries = entries.filter(f => !f.startsWith('._'));
|
|
30
|
+
return filterFn ? entries.filter(filterFn) : entries;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Same as cleanEntries but returns full paths.
|
|
35
|
+
*/
|
|
36
|
+
function cleanEntryPaths(dir, filterFn) {
|
|
37
|
+
return cleanEntries(dir, filterFn).map(f => path.join(dir, f));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Synchronous sleep (blocks the event loop) — used for tiny lock-retry
|
|
42
|
+
* backoffs where an async setTimeout would require restructuring a
|
|
43
|
+
* short-lived hook script into a promise chain for no real benefit.
|
|
44
|
+
* Falls back to a busy-wait if Atomics.wait is unavailable.
|
|
45
|
+
*/
|
|
46
|
+
function sleepSync(ms) {
|
|
47
|
+
try {
|
|
48
|
+
const sab = new SharedArrayBuffer(4);
|
|
49
|
+
Atomics.wait(new Int32Array(sab), 0, 0, ms);
|
|
50
|
+
} catch {
|
|
51
|
+
const end = Date.now() + ms;
|
|
52
|
+
while (Date.now() < end) { /* spin */ }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Write a file atomically: write to a unique temp path then rename into
|
|
58
|
+
* place. `fs.renameSync` is atomic on POSIX and Windows (same volume), so
|
|
59
|
+
* concurrent readers of `filePath` never observe a partially-written file
|
|
60
|
+
* (torn read) — they see either the old complete file or the new complete
|
|
61
|
+
* file, never a half-written one. Does not by itself prevent lost updates
|
|
62
|
+
* from concurrent read-modify-write cycles — pair with `claimLock` when
|
|
63
|
+
* that matters (see hook-latency.json / swarm-activity.json writers).
|
|
64
|
+
*/
|
|
65
|
+
function atomicWriteFileSync(filePath, data, encoding) {
|
|
66
|
+
const dir = path.dirname(filePath);
|
|
67
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch { /* ignore */ }
|
|
68
|
+
const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
69
|
+
fs.writeFileSync(tmp, data, encoding);
|
|
70
|
+
fs.renameSync(tmp, filePath);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Claim an exclusive lock file for short-lived critical sections (e.g. a
|
|
75
|
+
* rebuild-cooldown or spawn-once guard). Uses `wx` (exclusive create) so
|
|
76
|
+
* the claim itself is atomic — no read-check-write window.
|
|
77
|
+
*
|
|
78
|
+
* On contention, a lock older than `staleMs` is treated as abandoned (the
|
|
79
|
+
* holder crashed or never released it) and broken via an atomic RENAME
|
|
80
|
+
* (never unlink-then-create): renaming the stale lock path away is only
|
|
81
|
+
* ever won by exactly one racing process, so a concurrent process that
|
|
82
|
+
* also decided the lock was stale cannot delete *this* process's
|
|
83
|
+
* freshly-reclaimed lock out from under it (the TOCTOU race an
|
|
84
|
+
* unlink-then-create approach is vulnerable to). The loser of the rename
|
|
85
|
+
* race backs off and retries the whole claim sequence from the top rather
|
|
86
|
+
* than assuming it owns anything.
|
|
87
|
+
*
|
|
88
|
+
* Returns true if the lock was claimed (caller must eventually call
|
|
89
|
+
* `releaseLock`), false if another live/fresh owner holds it.
|
|
90
|
+
*/
|
|
91
|
+
function claimLock(lockPath, staleMs, maxAttempts) {
|
|
92
|
+
staleMs = staleMs || 30000;
|
|
93
|
+
maxAttempts = maxAttempts || 3;
|
|
94
|
+
try { fs.mkdirSync(path.dirname(lockPath), { recursive: true }); } catch { /* ignore */ }
|
|
95
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
96
|
+
try {
|
|
97
|
+
fs.writeFileSync(lockPath, String(process.pid), { flag: 'wx' });
|
|
98
|
+
return true;
|
|
99
|
+
} catch {
|
|
100
|
+
let stat;
|
|
101
|
+
try {
|
|
102
|
+
stat = fs.statSync(lockPath);
|
|
103
|
+
} catch {
|
|
104
|
+
// Lock vanished between our failed wx-create and this stat check —
|
|
105
|
+
// retry the create immediately.
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (Date.now() - stat.mtimeMs < staleMs) return false; // held by a live/fresh owner
|
|
109
|
+
|
|
110
|
+
const claimedName = `${lockPath}.${process.pid}.${Date.now()}.stale`;
|
|
111
|
+
try {
|
|
112
|
+
fs.renameSync(lockPath, claimedName);
|
|
113
|
+
} catch {
|
|
114
|
+
// Someone else already renamed the stale lock away — they're
|
|
115
|
+
// claiming it. Back off briefly and retry from the top rather than
|
|
116
|
+
// proceeding as if we own it.
|
|
117
|
+
sleepSync(5);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
try { fs.unlinkSync(claimedName); } catch { /* ignore */ }
|
|
121
|
+
// We won the rename race — loop back and attempt the wx-create fresh.
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Release a lock previously claimed by this process via `claimLock`. */
|
|
128
|
+
function releaseLock(lockPath) {
|
|
129
|
+
try {
|
|
130
|
+
if (Number(fs.readFileSync(lockPath, 'utf-8')) === process.pid) fs.unlinkSync(lockPath);
|
|
131
|
+
} catch { /* ignore */ }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Run `fn` while holding a lock (see `claimLock`) so a read-modify-write
|
|
136
|
+
* cycle is protected end-to-end, not just the final write. If the lock
|
|
137
|
+
* can't be claimed after retries, `fn` still runs (never block a hook) but
|
|
138
|
+
* without lost-update protection — best-effort, matching this codebase's
|
|
139
|
+
* "hooks must not block the session" rule.
|
|
140
|
+
*/
|
|
141
|
+
function withLock(lockPath, fn, staleMs, maxAttempts) {
|
|
142
|
+
const acquired = claimLock(lockPath, staleMs, maxAttempts);
|
|
143
|
+
try {
|
|
144
|
+
return fn();
|
|
145
|
+
} finally {
|
|
146
|
+
if (acquired) releaseLock(lockPath);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Append a JSONL line and rotate the file down to `maxLines` (keep the
|
|
152
|
+
* most recent lines) when it exceeds that count — mirrors the size-cap
|
|
153
|
+
* pattern already used for intelligence-outcomes.jsonl (500 lines) and
|
|
154
|
+
* episodes.jsonl. Prevents unbounded JSONL growth. Rotation write is
|
|
155
|
+
* atomic (tmp+rename).
|
|
156
|
+
*/
|
|
157
|
+
function appendJsonlWithRotation(filePath, line, maxLines) {
|
|
158
|
+
maxLines = maxLines || 500;
|
|
159
|
+
try { fs.mkdirSync(path.dirname(filePath), { recursive: true }); } catch { /* ignore */ }
|
|
160
|
+
fs.appendFileSync(filePath, line.endsWith('\n') ? line : line + '\n');
|
|
161
|
+
try {
|
|
162
|
+
const st = fs.statSync(filePath);
|
|
163
|
+
// Cheap heuristic to skip reading huge files line-by-line on every
|
|
164
|
+
// append: only bother counting lines once the file is already
|
|
165
|
+
// suspiciously large relative to a plausible maxLines * avg-line-size.
|
|
166
|
+
if (st.size < 4096) return;
|
|
167
|
+
const lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean);
|
|
168
|
+
if (lines.length > maxLines) {
|
|
169
|
+
atomicWriteFileSync(filePath, lines.slice(-maxLines).join('\n') + '\n', 'utf-8');
|
|
170
|
+
}
|
|
171
|
+
} catch { /* non-fatal — rotation is best-effort */ }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = {
|
|
175
|
+
cleanEntries,
|
|
176
|
+
cleanEntryPaths,
|
|
177
|
+
sleepSync,
|
|
178
|
+
atomicWriteFileSync,
|
|
179
|
+
claimLock,
|
|
180
|
+
releaseLock,
|
|
181
|
+
withLock,
|
|
182
|
+
appendJsonlWithRotation,
|
|
183
|
+
};
|
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
|
|
7
7
|
const { _openMonographDb } = require('./monograph.cjs');
|
|
8
|
+
const { cleanEntries, atomicWriteFileSync } = require('./fs-helpers.cjs');
|
|
8
9
|
|
|
9
10
|
const CWD = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
10
11
|
|
|
@@ -82,7 +83,7 @@ function _triggerCollectMdFiles(dir, _depth) {
|
|
|
82
83
|
if (depth > 5) return []; // cap recursion depth to prevent stack overflow DoS
|
|
83
84
|
var results = [];
|
|
84
85
|
try {
|
|
85
|
-
var entries =
|
|
86
|
+
var entries = cleanEntries(dir);
|
|
86
87
|
for (var i = 0; i < entries.length; i++) {
|
|
87
88
|
if (results.length >= 200) break; // cap total file count to prevent DoS
|
|
88
89
|
var full = path.join(dir, entries[i]);
|
|
@@ -135,8 +136,11 @@ function scanMicroAgentTriggers(prompt) {
|
|
|
135
136
|
if (!cacheLoaded) {
|
|
136
137
|
patterns = _triggerBuildIndex(agentDir);
|
|
137
138
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
// P2-21: tmp+rename — multiple concurrent hook processes (route-handler
|
|
140
|
+
// invocations fire per prompt/subagent) can all miss the cache at once
|
|
141
|
+
// and rebuild+write trigger-index.json simultaneously; atomic write
|
|
142
|
+
// prevents a concurrent reader from parsing a half-written file.
|
|
143
|
+
atomicWriteFileSync(indexPath, JSON.stringify({ patterns: patterns, builtAt: new Date().toISOString(), totalAgentsScanned: patterns.length }));
|
|
140
144
|
} catch (e) {}
|
|
141
145
|
}
|
|
142
146
|
|
|
@@ -334,9 +338,9 @@ function _autoIndexKnowledge(knowledgeDir) {
|
|
|
334
338
|
} catch (e) { /* graph not available yet */ }
|
|
335
339
|
|
|
336
340
|
try {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
341
|
+
// Same torn-read hazard as trigger-index.json above — atomic write.
|
|
342
|
+
atomicWriteFileSync(chunksFile, newLines.length > 0 ? newLines.join('\n') + '\n' : '', 'utf-8');
|
|
343
|
+
atomicWriteFileSync(hashFile, contentHash, 'utf-8');
|
|
340
344
|
} catch (e) {}
|
|
341
345
|
return newLines.length;
|
|
342
346
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const fs = require('fs');
|
|
7
|
+
const { atomicWriteFileSync, withLock, appendJsonlWithRotation } = require('./fs-helpers.cjs');
|
|
7
8
|
|
|
8
9
|
const CWD = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
9
10
|
|
|
@@ -19,7 +20,11 @@ function _recordRecentEdit(filePath) {
|
|
|
19
20
|
d.edits = d.edits.filter(function(e) { return e.file !== storedPath; });
|
|
20
21
|
d.edits.unshift({ file: storedPath, editedAt: Date.now() });
|
|
21
22
|
if (d.edits.length > 10) d.edits = d.edits.slice(0, 10);
|
|
22
|
-
|
|
23
|
+
// P2-21: tmp+rename so a concurrent reader of recent-edits.json (fired by
|
|
24
|
+
// another PostToolUse hook process in the same batched Edit) never sees a
|
|
25
|
+
// partially-written file. Last-writer-wins on the merge itself is
|
|
26
|
+
// acceptable here — losing one recent-edit entry is low-stakes.
|
|
27
|
+
atomicWriteFileSync(f, JSON.stringify(d));
|
|
23
28
|
} catch (e) { /* non-fatal */ }
|
|
24
29
|
}
|
|
25
30
|
|
|
@@ -45,7 +50,8 @@ function _recordToolCall(signature) {
|
|
|
45
50
|
d = { startedAt: Date.now(), calls: {} };
|
|
46
51
|
}
|
|
47
52
|
d.calls[signature] = (d.calls[signature] || 0) + 1;
|
|
48
|
-
|
|
53
|
+
// P2-21: atomic write — see _recordRecentEdit above for rationale.
|
|
54
|
+
atomicWriteFileSync(f, JSON.stringify(d));
|
|
49
55
|
return d.calls[signature];
|
|
50
56
|
} catch (e) { return 0; }
|
|
51
57
|
}
|
|
@@ -77,8 +83,8 @@ function _getBudgetStatus() {
|
|
|
77
83
|
monthlyLimit = Math.max(monthlyLimit || 0, Math.ceil(dailyAvg * 1.5 * 30));
|
|
78
84
|
autoTuned = true;
|
|
79
85
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
// P2-21: atomic write — see _recordRecentEdit above for rationale.
|
|
87
|
+
atomicWriteFileSync(budgetFile, JSON.stringify({
|
|
82
88
|
dailyLimit: dailyLimit, monthlyLimit: monthlyLimit,
|
|
83
89
|
autoTuned: true, tunedAt: now.toISOString(),
|
|
84
90
|
basis: 'rolling avg $' + dailyAvg.toFixed(2) + '/day × 1.5',
|
|
@@ -106,20 +112,28 @@ function _getBudgetStatus() {
|
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
function _recordHookLatency(handlerName, durationMs) {
|
|
115
|
+
var f = path.join(CWD, '.monomind', 'metrics', 'hook-latency.json');
|
|
116
|
+
// P2-21: hook-latency.json's per-handler count/total/max are aggregated
|
|
117
|
+
// counters written by every hook invocation — a plain read-modify-write
|
|
118
|
+
// silently drops whichever concurrent process wrote last (lost update),
|
|
119
|
+
// not just torn reads. Wrap the whole cycle in a lock (see
|
|
120
|
+
// utils/fs-helpers.cjs claimLock/withLock, same TOCTOU-safe pattern used
|
|
121
|
+
// for the rebuild/spawn locks) so the read and the write happen as one
|
|
122
|
+
// critical section instead of racing.
|
|
109
123
|
try {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
124
|
+
withLock(f + '.lock', function() {
|
|
125
|
+
var d = {};
|
|
126
|
+
try { d = JSON.parse(fs.readFileSync(f, 'utf-8')); } catch (_) {}
|
|
127
|
+
if (typeof d !== 'object' || d === null) d = {};
|
|
128
|
+
var entry = d[handlerName] || { count: 0, total: 0, max: 0 };
|
|
129
|
+
entry.count++;
|
|
130
|
+
entry.total += durationMs;
|
|
131
|
+
entry.max = Math.max(entry.max, durationMs);
|
|
132
|
+
entry.mean = Math.round(entry.total / entry.count);
|
|
133
|
+
d[handlerName] = entry;
|
|
134
|
+
d.lastUpdated = Date.now();
|
|
135
|
+
atomicWriteFileSync(f, JSON.stringify(d));
|
|
136
|
+
}, 5000);
|
|
123
137
|
} catch (e) {}
|
|
124
138
|
}
|
|
125
139
|
|
|
@@ -130,9 +144,11 @@ function _recordDecisionMarkers(promptText) {
|
|
|
130
144
|
if (!matches || matches.length === 0) return;
|
|
131
145
|
try {
|
|
132
146
|
var f = path.join(CWD, '.monomind', 'decisions.jsonl');
|
|
133
|
-
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
134
147
|
var entry = JSON.stringify({ ts: Date.now(), excerpts: matches.slice(0, 3), prompt: promptText.slice(0, 400) });
|
|
135
|
-
|
|
148
|
+
// P2-26: decisions.jsonl was a pure appendFileSync with no rotation —
|
|
149
|
+
// unlike intelligence-outcomes.jsonl (capped at 500 lines). Cap it the
|
|
150
|
+
// same way so it can't grow unbounded across a long project history.
|
|
151
|
+
appendJsonlWithRotation(f, entry, 500);
|
|
136
152
|
} catch (e) {}
|
|
137
153
|
}
|
|
138
154
|
|
|
@@ -241,8 +241,17 @@ if (isMCPMode) {
|
|
|
241
241
|
|
|
242
242
|
const { CLI } = await import('../dist/src/index.js');
|
|
243
243
|
const cli = new CLI();
|
|
244
|
+
// A detached daemon child (`start --daemon --foreground-worker-internal`,
|
|
245
|
+
// see src/commands/start.ts) intentionally keeps a ref'd setInterval alive
|
|
246
|
+
// after its action resolves — that's what holds the process open as a real
|
|
247
|
+
// daemon. Unconditionally calling process.exit() here would kill it the
|
|
248
|
+
// instant the action's promise resolves, defeating the whole point.
|
|
249
|
+
const isDaemonChild = cliArgs.includes('--foreground-worker-internal');
|
|
244
250
|
cli.run().then(() => {
|
|
245
|
-
|
|
251
|
+
if (!isDaemonChild) {
|
|
252
|
+
process.exit(process.exitCode ?? 0);
|
|
253
|
+
}
|
|
254
|
+
// Daemon child: let the event loop stay alive on its own ref'd interval.
|
|
246
255
|
}).catch((error) => {
|
|
247
256
|
console.error('Fatal error:', safeMsg(error && error.message));
|
|
248
257
|
process.exit(1);
|
|
@@ -8,11 +8,7 @@ import { formatStatus } from './agent-lifecycle.js';
|
|
|
8
8
|
export const metricsCommand = {
|
|
9
9
|
name: 'metrics',
|
|
10
10
|
description: 'Show agent performance metrics',
|
|
11
|
-
options: [
|
|
12
|
-
{ name: 'period', short: 'p', description: 'Time period (1h, 24h, 7d, 30d)', type: 'string', default: '24h' },
|
|
13
|
-
],
|
|
14
11
|
action: async (ctx) => {
|
|
15
|
-
const period = ctx.flags.period;
|
|
16
12
|
const { existsSync, readFileSync, readdirSync, statSync } = await import('fs');
|
|
17
13
|
const { join } = await import('path');
|
|
18
14
|
let totalAgents = 0;
|
|
@@ -61,13 +57,12 @@ export const metricsCommand = {
|
|
|
61
57
|
catch { /* ignore */ }
|
|
62
58
|
}
|
|
63
59
|
let vectorCount = 0;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
catch { /* ignore */ }
|
|
60
|
+
try {
|
|
61
|
+
const { bridgeGetBackendStats } = await import('../memory/memory-bridge.js');
|
|
62
|
+
const backendStats = await bridgeGetBackendStats();
|
|
63
|
+
vectorCount = backendStats?.totalEntries ?? 0;
|
|
70
64
|
}
|
|
65
|
+
catch { /* backend unavailable */ }
|
|
71
66
|
const byType = Object.entries(typeCounts).map(([type, data]) => ({
|
|
72
67
|
type, count: data.count, tasks: data.tasks,
|
|
73
68
|
successRate: data.tasks > 0 ? `${Math.round((data.success / data.tasks) * 100)}%` : 'N/A',
|
|
@@ -76,20 +71,19 @@ export const metricsCommand = {
|
|
|
76
71
|
? `${Math.round(Object.values(typeCounts).reduce((a, d) => a + d.success, 0) / tasksCompleted * 100)}%`
|
|
77
72
|
: 'N/A';
|
|
78
73
|
const metrics = {
|
|
79
|
-
period,
|
|
80
74
|
summary: {
|
|
81
75
|
totalAgents, activeAgents, tasksCompleted, avgSuccessRate, vectorCount,
|
|
82
76
|
note: totalAgents === 0 ? 'No agents spawned yet. Use: agent spawn -t coder' : undefined,
|
|
83
77
|
},
|
|
84
78
|
byType,
|
|
85
|
-
performance: {
|
|
79
|
+
performance: { memoryEntries: `${vectorCount} entries`, searchBackend: vectorCount > 0 ? 'LanceDB' : 'none' },
|
|
86
80
|
};
|
|
87
81
|
if (ctx.flags.format === 'json') {
|
|
88
82
|
output.printJson(metrics);
|
|
89
83
|
return { success: true, data: metrics };
|
|
90
84
|
}
|
|
91
85
|
output.writeln();
|
|
92
|
-
output.writeln(output.bold(
|
|
86
|
+
output.writeln(output.bold('Agent Metrics'));
|
|
93
87
|
output.writeln();
|
|
94
88
|
output.printTable({
|
|
95
89
|
columns: [
|
|
@@ -101,7 +95,7 @@ export const metricsCommand = {
|
|
|
101
95
|
{ metric: 'Active Agents', value: metrics.summary.activeAgents },
|
|
102
96
|
{ metric: 'Tasks Completed', value: metrics.summary.tasksCompleted },
|
|
103
97
|
{ metric: 'Success Rate', value: metrics.summary.avgSuccessRate },
|
|
104
|
-
{ metric: 'Memory
|
|
98
|
+
{ metric: 'Memory Entries', value: metrics.summary.vectorCount },
|
|
105
99
|
],
|
|
106
100
|
});
|
|
107
101
|
output.writeln();
|
|
@@ -122,7 +116,7 @@ export const metricsCommand = {
|
|
|
122
116
|
output.writeln();
|
|
123
117
|
output.writeln(output.bold('Memory'));
|
|
124
118
|
output.printList([
|
|
125
|
-
`
|
|
119
|
+
`Entries: ${output.success(metrics.performance.memoryEntries)}`,
|
|
126
120
|
`Backend: ${output.success(metrics.performance.searchBackend)}`,
|
|
127
121
|
]);
|
|
128
122
|
return { success: true, data: metrics };
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* github.com/monoes/monomind
|
|
6
6
|
*/
|
|
7
7
|
import { output } from '../output.js';
|
|
8
|
-
import { existsSync, lstatSync, rmSync, readdirSync } from 'fs';
|
|
9
|
-
import { join } from 'path';
|
|
8
|
+
import { existsSync, lstatSync, rmSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
9
|
+
import { join, dirname } from 'path';
|
|
10
10
|
/**
|
|
11
11
|
* Artifact directories and files that monomind/monomind may create
|
|
12
12
|
*/
|
|
@@ -190,13 +190,29 @@ export const cleanupCommand = {
|
|
|
190
190
|
// Actually delete
|
|
191
191
|
try {
|
|
192
192
|
const fullPath = join(cwd, item.path);
|
|
193
|
-
|
|
193
|
+
// Special-case: `.claude/` is scanned and removed as a single unit,
|
|
194
|
+
// but KEEP_CONFIG_PATHS promises `.claude/settings.json` survives
|
|
195
|
+
// --keep-config. Back the file up, wipe the directory, then
|
|
196
|
+
// restore just that one file — the rest of `.claude/` is still
|
|
197
|
+
// removed as normal.
|
|
198
|
+
const settingsPath = join(cwd, '.claude', 'settings.json');
|
|
199
|
+
const isClaudeDirWithPreservedSettings = item.type === 'dir' && item.path === '.claude' && keepConfig && existsSync(settingsPath);
|
|
200
|
+
if (isClaudeDirWithPreservedSettings) {
|
|
201
|
+
const settingsBackup = readFileSync(settingsPath);
|
|
194
202
|
rmSync(fullPath, { recursive: true, force: true });
|
|
203
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
204
|
+
writeFileSync(settingsPath, settingsBackup);
|
|
205
|
+
output.writeln(output.success(` [removed] ${typeLabel} ${item.path} (${sizeStr}) - ${item.description}`));
|
|
206
|
+
output.writeln(output.dim(` [kept] file .claude/settings.json - preserved (--keep-config)`));
|
|
207
|
+
}
|
|
208
|
+
else if (item.type === 'dir') {
|
|
209
|
+
rmSync(fullPath, { recursive: true, force: true });
|
|
210
|
+
output.writeln(output.success(` [removed] ${typeLabel} ${item.path} (${sizeStr}) - ${item.description}`));
|
|
195
211
|
}
|
|
196
212
|
else {
|
|
197
213
|
rmSync(fullPath, { force: true });
|
|
214
|
+
output.writeln(output.success(` [removed] ${typeLabel} ${item.path} (${sizeStr}) - ${item.description}`));
|
|
198
215
|
}
|
|
199
|
-
output.writeln(output.success(` [removed] ${typeLabel} ${item.path} (${sizeStr}) - ${item.description}`));
|
|
200
216
|
removedCount++;
|
|
201
217
|
removedSize += item.size;
|
|
202
218
|
}
|