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
|
@@ -32,8 +32,13 @@ const SECRET_PATTERNS = [
|
|
|
32
32
|
/(?:api[_-]?key|apikey)\s*[:=]\s*['"][^'"]{8,}['"]/gi,
|
|
33
33
|
/(?:secret|password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]/gi,
|
|
34
34
|
/(?:token|bearer)\s*[:=]\s*['"][^'"]{10,}['"]/gi,
|
|
35
|
+
// Unquoted variants — env-style KEY equals value with no surrounding
|
|
36
|
+
// quotes (the single most common real leak pattern; the quoted patterns
|
|
37
|
+
// above never match a bare assignment in a shell export or .env file).
|
|
38
|
+
/(?:api[_-]?key|apikey|token|secret|password|passwd|pwd)\s*[:=]\s*[^\s'"]{8,}/gi,
|
|
35
39
|
/-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g,
|
|
36
|
-
/sk-[a-zA-Z0-
|
|
40
|
+
/sk-ant-[a-zA-Z0-9_-]{20,}/g,
|
|
41
|
+
/sk-[a-zA-Z0-9_-]{20,}/g,
|
|
37
42
|
/ghp_[a-zA-Z0-9]{36}/g,
|
|
38
43
|
/npm_[a-zA-Z0-9]{36}/g,
|
|
39
44
|
/AKIA[0-9A-Z]{16}/g,
|
|
@@ -92,11 +97,14 @@ function handlePreBash(hCtx) {
|
|
|
92
97
|
|
|
93
98
|
var result = checkDestructive(cmd);
|
|
94
99
|
if (result.triggered) {
|
|
95
|
-
//
|
|
96
|
-
|
|
100
|
+
// Set exit code 2 to block, and write the reason to STDERR — per Claude
|
|
101
|
+
// Code's PreToolUse hook protocol, stdout JSON is only parsed when exit
|
|
102
|
+
// code is 0; at exit code 2 the caller reads the block reason from
|
|
103
|
+
// stderr instead, so putting it on stdout here would make it invisible.
|
|
104
|
+
process.stderr.write(JSON.stringify({
|
|
97
105
|
decision: 'block',
|
|
98
106
|
reason: '[gates] ' + result.reason,
|
|
99
|
-
}));
|
|
107
|
+
}) + '\n');
|
|
100
108
|
process.exitCode = 2;
|
|
101
109
|
}
|
|
102
110
|
}
|
|
@@ -120,11 +128,12 @@ function handlePreWrite(hCtx) {
|
|
|
120
128
|
|
|
121
129
|
var result = checkSecrets(content);
|
|
122
130
|
if (result.triggered) {
|
|
123
|
-
//
|
|
124
|
-
|
|
131
|
+
// Set exit code 2 to block, and write the reason to STDERR — see the
|
|
132
|
+
// matching comment in handlePreBash for why stdout is the wrong stream.
|
|
133
|
+
process.stderr.write(JSON.stringify({
|
|
125
134
|
decision: 'block',
|
|
126
135
|
reason: '[gates] ' + result.reason,
|
|
127
|
-
}));
|
|
136
|
+
}) + '\n');
|
|
128
137
|
process.exitCode = 2;
|
|
129
138
|
}
|
|
130
139
|
}
|
|
@@ -10,7 +10,7 @@ module.exports = {
|
|
|
10
10
|
var loopsDir = path.join(CWD, '.monomind', 'loops');
|
|
11
11
|
if (!fs.existsSync(loopsDir)) { console.log('No loops directory.'); return; }
|
|
12
12
|
var files = fs.readdirSync(loopsDir).filter(function(f) {
|
|
13
|
-
return f.endsWith('.json') && !f.includes('-hil') && !f.endsWith('.stop');
|
|
13
|
+
return f.endsWith('.json') && !f.includes('-hil') && !f.endsWith('.stop') && !f.startsWith('._');
|
|
14
14
|
}).slice(0, 200); // cap to prevent DoS via many files
|
|
15
15
|
var STALE_MS = 6 * 60 * 60 * 1000;
|
|
16
16
|
var now = Date.now();
|
|
@@ -327,15 +327,15 @@ module.exports = {
|
|
|
327
327
|
}
|
|
328
328
|
}
|
|
329
329
|
}
|
|
330
|
-
// Performance metrics (
|
|
330
|
+
// Performance metrics (optimize worker — a one-shot process, not a daemon)
|
|
331
331
|
var perfFile = path.join(metricsDir, 'performance.json');
|
|
332
332
|
if (fs.existsSync(perfFile) && fs.statSync(perfFile).size < 32768) {
|
|
333
333
|
var perfData = JSON.parse(fs.readFileSync(perfFile, 'utf-8'));
|
|
334
|
-
if (perfData && perfData.
|
|
335
|
-
var rssBytes = perfData.
|
|
334
|
+
if (perfData && perfData.workerProcessMemoryUsage) {
|
|
335
|
+
var rssBytes = perfData.workerProcessMemoryUsage.rss || 0;
|
|
336
336
|
var rssMB = Math.round(rssBytes / (1024 * 1024));
|
|
337
337
|
if (rssMB > 512) {
|
|
338
|
-
console.log('[PERF]
|
|
338
|
+
console.log('[PERF] optimize worker RSS ' + rssMB + 'MB (>512MB threshold) at last run — reflects that one-shot process, not a persistent daemon');
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
341
|
}
|
|
@@ -89,17 +89,48 @@ module.exports = {
|
|
|
89
89
|
// Only write the success flag when derived from actual evidence (commits, outcomes)
|
|
90
90
|
if (typeof sessionSuccess === 'boolean') feedbackEntry.intelligenceFeedback = sessionSuccess;
|
|
91
91
|
fs.appendFileSync(feedbackPath, JSON.stringify(feedbackEntry) + '\n', 'utf-8');
|
|
92
|
-
// Rotate: keep last 1000 lines to prevent unbounded growth
|
|
92
|
+
// Rotate: keep last 1000 lines to prevent unbounded growth.
|
|
93
93
|
try {
|
|
94
94
|
var MAX_FEEDBACK = 512 * 1024; // 512 KiB
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
95
|
+
var feedbackStat = fs.existsSync(feedbackPath) ? fs.statSync(feedbackPath) : null;
|
|
96
|
+
if (feedbackStat && feedbackStat.size > MAX_FEEDBACK) {
|
|
97
|
+
// Emergency tail-trim: the file is too large to comfortably read+split
|
|
98
|
+
// in full, so seek to a byte offset near the end, read only that tail,
|
|
99
|
+
// drop the first (possibly partial) line, and keep the rest — instead
|
|
100
|
+
// of the previous behavior which threw here and skipped trimming
|
|
101
|
+
// entirely, leaving the file to grow forever and permanently disabling
|
|
102
|
+
// consumers (router.cjs, route-handler.cjs) that bail out on oversized
|
|
103
|
+
// files.
|
|
104
|
+
var TAIL_BYTES = 256 * 1024; // ~256 KiB of tail content
|
|
105
|
+
var fd = fs.openSync(feedbackPath, 'r');
|
|
106
|
+
try {
|
|
107
|
+
var readSize = Math.min(TAIL_BYTES, feedbackStat.size);
|
|
108
|
+
var startOffset = feedbackStat.size - readSize;
|
|
109
|
+
var buf = Buffer.alloc(readSize);
|
|
110
|
+
fs.readSync(fd, buf, 0, readSize, startOffset);
|
|
111
|
+
var tailRaw = buf.toString('utf-8');
|
|
112
|
+
var tailLines = tailRaw.split('\n');
|
|
113
|
+
// Drop the first line if we didn't start at byte 0 — it's likely partial.
|
|
114
|
+
if (startOffset > 0) tailLines.shift();
|
|
115
|
+
var validTailLines = tailLines.filter(function(l) {
|
|
116
|
+
if (!l) return false;
|
|
117
|
+
try { JSON.parse(l); return true; } catch (eParse) { return false; }
|
|
118
|
+
});
|
|
119
|
+
var trimmedTail = validTailLines.slice(-1000);
|
|
120
|
+
var tmpFeedbackPath = feedbackPath + '.' + process.pid + '.tmp';
|
|
121
|
+
fs.writeFileSync(tmpFeedbackPath, trimmedTail.join('\n') + (trimmedTail.length > 0 ? '\n' : ''), 'utf-8');
|
|
122
|
+
fs.renameSync(tmpFeedbackPath, feedbackPath);
|
|
123
|
+
} finally {
|
|
124
|
+
fs.closeSync(fd);
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
var raw = fs.readFileSync(feedbackPath, 'utf-8');
|
|
128
|
+
var lines = raw.split('\n').filter(Boolean);
|
|
129
|
+
if (lines.length > 1000) {
|
|
130
|
+
var tmpFeedbackPath2 = feedbackPath + '.' + process.pid + '.tmp';
|
|
131
|
+
fs.writeFileSync(tmpFeedbackPath2, lines.slice(-1000).join('\n') + '\n', 'utf-8');
|
|
132
|
+
fs.renameSync(tmpFeedbackPath2, feedbackPath);
|
|
133
|
+
}
|
|
103
134
|
}
|
|
104
135
|
} catch (e2) { /* rotation is best-effort */ }
|
|
105
136
|
}
|
|
@@ -141,19 +172,6 @@ module.exports = {
|
|
|
141
172
|
|
|
142
173
|
// Memory Palace tombstone writes removed — redundant with raw session JSONL
|
|
143
174
|
|
|
144
|
-
// ── Context Persistence Auto-Archive ─────────────────────────────────
|
|
145
|
-
// Archive conversation context so it survives compaction and new sessions
|
|
146
|
-
try {
|
|
147
|
-
var cpHook = await import('file://' + path.join(__dirname, '..', 'context-persistence-hook.mjs'));
|
|
148
|
-
if (cpHook && cpHook.archive) {
|
|
149
|
-
await hCtx.runWithTimeout(function() { return cpHook.archive(); }, 'context-persistence.archive()');
|
|
150
|
-
console.log('[CONTEXT_PERSIST] Session transcript archived');
|
|
151
|
-
} else if (cpHook && cpHook.default && cpHook.default.archive) {
|
|
152
|
-
await hCtx.runWithTimeout(function() { return cpHook.default.archive(); }, 'context-persistence.archive()');
|
|
153
|
-
console.log('[CONTEXT_PERSIST] Session transcript archived');
|
|
154
|
-
}
|
|
155
|
-
} catch (e) { /* non-fatal — context-persistence may not export archive() */ }
|
|
156
|
-
|
|
157
175
|
// ── Auto-populate: write session episode for every session ─────
|
|
158
176
|
// Even solo sessions (no subagents) produce an episode so the
|
|
159
177
|
// route handler can surface relevant past sessions.
|
|
@@ -206,18 +206,6 @@ module.exports = {
|
|
|
206
206
|
}
|
|
207
207
|
} catch (e) { /* non-fatal — hooks workers unavailable */ }
|
|
208
208
|
|
|
209
|
-
// Context Persistence Auto-Restore
|
|
210
|
-
try {
|
|
211
|
-
var cpHook = await import('file://' + path.join(helpersDir, 'context-persistence-hook.mjs'));
|
|
212
|
-
var restoreFn = (cpHook && cpHook.restore) || (cpHook && cpHook.default && cpHook.default.restore);
|
|
213
|
-
if (restoreFn) {
|
|
214
|
-
var restored = await runWithTimeout(function() { return restoreFn(); }, 'context-persistence.restore()');
|
|
215
|
-
if (restored && restored.turns > 0) {
|
|
216
|
-
console.log('[CONTEXT_RESTORED] ' + restored.turns + ' turns from previous session');
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
} catch (e) { /* non-fatal */ }
|
|
220
|
-
|
|
221
209
|
// AgentKnowledgeBase — preload shared knowledge context on session restore.
|
|
222
210
|
try {
|
|
223
211
|
var knowledgeDir = path.join(CWD, '.monomind', 'knowledge');
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const fs = require('fs');
|
|
8
|
+
const { purgeStaleRegistrations } = require('../utils/agent-registrations.cjs');
|
|
9
|
+
const { atomicWriteFileSync, withLock } = require('../utils/fs-helpers.cjs');
|
|
8
10
|
|
|
9
11
|
module.exports = {
|
|
10
12
|
handlePreTask: async function(hCtx) {
|
|
@@ -50,36 +52,65 @@ module.exports = {
|
|
|
50
52
|
intelligence.feedback(taskSuccess);
|
|
51
53
|
} catch (e) { /* non-fatal */ }
|
|
52
54
|
}
|
|
53
|
-
// Each TeammateIdle/TaskCompleted = one agent done → remove
|
|
55
|
+
// Each TeammateIdle/TaskCompleted = one agent done → remove its registration.
|
|
54
56
|
const regDir = path.join(CWD, '.monomind', 'agents', 'registrations');
|
|
55
57
|
try {
|
|
56
58
|
if (fs.existsSync(regDir)) {
|
|
57
59
|
const files = fs.readdirSync(regDir).filter(f => f.endsWith('.json'));
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
// P3-15: post-task (TeammateIdle/TaskCompleted) also fires for the MAIN
|
|
61
|
+
// session's own tasks — e.g. the team lead completing a self-assigned
|
|
62
|
+
// TaskList item — which never wrote an agent-start registration in the
|
|
63
|
+
// first place. Blindly popping the oldest registration on EVERY
|
|
64
|
+
// post-task event therefore deregistered an unrelated, still-running
|
|
65
|
+
// agent whenever the lead (not a subagent) finished a task, silently
|
|
66
|
+
// drifting the count negative-equivalent (undercounting active agents).
|
|
67
|
+
//
|
|
68
|
+
// Registrations now carry an `agentType` (agent-start-handler.cjs), and
|
|
69
|
+
// this hook's payload may carry the same identifying field under one of
|
|
70
|
+
// several possible names — mirroring the precedence agent-start-handler
|
|
71
|
+
// already uses to derive it. If this event carries NO such field at
|
|
72
|
+
// all, there's no evidence it corresponds to a real agent completion
|
|
73
|
+
// (as opposed to the main session's own task), so we skip touching
|
|
74
|
+
// registrations entirely rather than guessing via FIFO. When the field
|
|
75
|
+
// IS present, prefer removing the registration recorded with the SAME
|
|
76
|
+
// type; fall back to oldest-of-all only if no type match is found
|
|
77
|
+
// (e.g. a legacy registration written before this fix, with no
|
|
78
|
+
// agentType stored).
|
|
79
|
+
const completingType = hookInput.subagent_type || hookInput.agentType || hookInput.agent_type
|
|
80
|
+
|| hookInput.agentSlug || hookInput.agent_slug || '';
|
|
81
|
+
if (files.length > 0 && completingType) {
|
|
60
82
|
const sorted = files
|
|
61
|
-
.map(f =>
|
|
83
|
+
.map(f => {
|
|
84
|
+
let mtime = 0, agentType = null;
|
|
85
|
+
try { mtime = fs.statSync(path.join(regDir, f)).mtimeMs; } catch { /* ignore */ }
|
|
86
|
+
try { agentType = JSON.parse(fs.readFileSync(path.join(regDir, f), 'utf-8')).agentType || null; } catch { /* ignore */ }
|
|
87
|
+
return { f, mtime, agentType };
|
|
88
|
+
})
|
|
62
89
|
.sort((a, b) => a.mtime - b.mtime);
|
|
63
|
-
|
|
90
|
+
const typeMatch = sorted.find(r => r.agentType === completingType);
|
|
91
|
+
const toRemove = typeMatch || sorted[0];
|
|
92
|
+
try { fs.unlinkSync(path.join(regDir, toRemove.f)); } catch { /* ignore */ }
|
|
64
93
|
}
|
|
65
|
-
// Also purge any stragglers older than 30 min
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
94
|
+
// Also purge any stragglers older than 30 min (shared with agent-start-handler.cjs)
|
|
95
|
+
const remaining = purgeStaleRegistrations(regDir) || 0;
|
|
96
|
+
// P2-21: shares a lock with agent-start-handler.cjs's swarm-activity.json
|
|
97
|
+
// writer (same path + '.lock') so the read-prevLastActive-then-write
|
|
98
|
+
// cycle can't race with that handler's concurrent write and silently
|
|
99
|
+
// drop whichever one wrote last — see that file for full rationale.
|
|
71
100
|
const _actPath = path.join(CWD, '.monomind', 'metrics', 'swarm-activity.json');
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
101
|
+
withLock(_actPath + '.lock', function() {
|
|
102
|
+
let _prevLastActive = 0;
|
|
103
|
+
try { var _actSt = fs.statSync(_actPath); if (_actSt.size < 65536) { _prevLastActive = (JSON.parse(fs.readFileSync(_actPath, 'utf-8'))?.swarm?.lastActive) || 0; } } catch { /* ignore */ }
|
|
104
|
+
atomicWriteFileSync(_actPath, JSON.stringify({
|
|
105
|
+
timestamp: new Date().toISOString(),
|
|
106
|
+
swarm: {
|
|
107
|
+
active: remaining > 0,
|
|
108
|
+
agent_count: remaining,
|
|
109
|
+
coordination_active: remaining > 0,
|
|
110
|
+
lastActive: Math.max(remaining, _prevLastActive), // preserve peak across completion
|
|
111
|
+
},
|
|
112
|
+
}));
|
|
113
|
+
}, 5000);
|
|
83
114
|
}
|
|
84
115
|
} catch (e) { /* non-fatal */ }
|
|
85
116
|
|
|
@@ -136,9 +136,49 @@ async function _ensureHooksModule() {
|
|
|
136
136
|
_hooksModule = null; // not built — bridge stays a fail-soft no-op
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
|
+
if (!_hooksModule) _warnHooksBridgeUnavailable();
|
|
139
140
|
return _hooksModule;
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
// ── Visible one-time warning when the @monomind/hooks bridge fully fails ───
|
|
144
|
+
// Both load paths above (bare specifier + dev-repo file:// fallback) failed:
|
|
145
|
+
// the 15 background workers (metrics, security scan, DDD tracking, etc.) are
|
|
146
|
+
// unavailable for the rest of this session. This is almost always because
|
|
147
|
+
// @monomind/hooks was never resolvable from npm (it ships with
|
|
148
|
+
// optionalDependencies:"*", which npm silently skips) — NOT a problem with
|
|
149
|
+
// the user's own project setup. Historically this failed completely
|
|
150
|
+
// silently (see docs/AUDIT-BACKLOG.md P1-1), burning debugging time on the
|
|
151
|
+
// wrong target. Warned once per session (file-marker keyed by session id,
|
|
152
|
+
// same pattern as the once-per-day update check below) since every hook
|
|
153
|
+
// event is its own fresh `node` process and would otherwise spam this on
|
|
154
|
+
// every single tool call.
|
|
155
|
+
function _warnHooksBridgeUnavailable() {
|
|
156
|
+
try {
|
|
157
|
+
var sessId = String((hookInput && (hookInput.sessionId || hookInput.session_id)) || '');
|
|
158
|
+
var flagFile = path.join(CWD, '.monomind', 'hooks-bridge-warned.json');
|
|
159
|
+
var alreadyWarned = false;
|
|
160
|
+
if (sessId && fs.existsSync(flagFile)) {
|
|
161
|
+
try {
|
|
162
|
+
var flagData = JSON.parse(fs.readFileSync(flagFile, 'utf-8'));
|
|
163
|
+
if (flagData && flagData.sessionId === sessId) alreadyWarned = true;
|
|
164
|
+
} catch (e) { /* corrupt/unreadable flag file — warn again to be safe */ }
|
|
165
|
+
}
|
|
166
|
+
if (alreadyWarned) return;
|
|
167
|
+
process.stderr.write(
|
|
168
|
+
'[WARN] @monomind/hooks could not be loaded (bare specifier and dev-repo fallback both failed) — ' +
|
|
169
|
+
'background workers (metrics, security scan, DDD tracking, etc.) are unavailable this session. ' +
|
|
170
|
+
'This is not a problem with your project configuration — the package is not resolvable via npm ' +
|
|
171
|
+
'from this install. See docs/AUDIT-BACKLOG.md P1-1 or https://github.com/monoes/monomind/issues.\n'
|
|
172
|
+
);
|
|
173
|
+
if (sessId) {
|
|
174
|
+
try {
|
|
175
|
+
fs.mkdirSync(path.join(CWD, '.monomind'), { recursive: true });
|
|
176
|
+
fs.writeFileSync(flagFile, JSON.stringify({ sessionId: sessId, warnedAt: new Date().toISOString() }), 'utf-8');
|
|
177
|
+
} catch (e) { /* non-fatal — worst case we warn again next event */ }
|
|
178
|
+
}
|
|
179
|
+
} catch (e) { /* never let the warning itself break a hook event */ }
|
|
180
|
+
}
|
|
181
|
+
|
|
142
182
|
// ── Intelligence timeout protection (fixes #1530, #1531) ───────────────────
|
|
143
183
|
var INTELLIGENCE_TIMEOUT_MS = 1500;
|
|
144
184
|
function runWithTimeout(fn, label) {
|
|
@@ -351,7 +391,20 @@ const handlers = {
|
|
|
351
391
|
h.handle(hCtx);
|
|
352
392
|
},
|
|
353
393
|
|
|
354
|
-
'pre-bash': () => {
|
|
394
|
+
'pre-bash': async () => {
|
|
395
|
+
// SECURITY GATE FIRST — destructive-ops enforcement must never be
|
|
396
|
+
// starved by the slower enrichment work below (monograph hint lookups).
|
|
397
|
+
// Previously this ran LAST, after up to 10 monograph SQLite strategies,
|
|
398
|
+
// AND was fired without awaiting its promise — meaning the block
|
|
399
|
+
// decision (process.exitCode) could still be unset when the process
|
|
400
|
+
// exited under the global 5s safety timer (see main()'s
|
|
401
|
+
// `process.exit(0)`), i.e. the gate could fail OPEN under time
|
|
402
|
+
// pressure. Awaiting it first guarantees the block/allow decision is
|
|
403
|
+
// computed and set before any enrichment work even starts.
|
|
404
|
+
var gates = require('./handlers/gates-handler.cjs');
|
|
405
|
+
await gates.handlePreBash(hCtx);
|
|
406
|
+
if (process.exitCode === 2) return; // blocked — skip enrichment entirely
|
|
407
|
+
|
|
355
408
|
var cmd = (hCtx.toolInput && (hCtx.toolInput.command || hCtx.toolInput.cmd)) || '';
|
|
356
409
|
var isGrep = /\b(?:grep|rg|ag)\b/.test(cmd);
|
|
357
410
|
var isFind = /\b(?:find|fd)\b/.test(cmd) && !isGrep;
|
|
@@ -596,9 +649,6 @@ const handlers = {
|
|
|
596
649
|
else if (isGrep) _recordGraphTelemetry('bash_grep_call');
|
|
597
650
|
else _recordGraphTelemetry('bash_find_call');
|
|
598
651
|
}
|
|
599
|
-
// Enforcement gate: destructive operations
|
|
600
|
-
var gates = require('./handlers/gates-handler.cjs');
|
|
601
|
-
gates.handlePreBash(hCtx);
|
|
602
652
|
},
|
|
603
653
|
|
|
604
654
|
'pre-write': () => {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const fs = require('fs');
|
|
16
|
+
const { claimLock, releaseLock } = require('./utils/fs-helpers.cjs');
|
|
16
17
|
|
|
17
18
|
// Resolve base dir at require-time so tests can inject CLAUDE_PROJECT_DIR
|
|
18
19
|
// per fresh require() call.
|
|
@@ -35,6 +36,76 @@ function ensureDataDir() {
|
|
|
35
36
|
try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch (_) {}
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
// ── multi-process-safe store flush (P1-14) ──────────────────────────────────
|
|
40
|
+
// STORE_FILE (auto-memory-store.json) is read once into `_entries` at init()
|
|
41
|
+
// and can be written by more than one code path — consolidate() and
|
|
42
|
+
// bootstrapFromDb() — each of which may be running in a *different* process
|
|
43
|
+
// (e.g. the long-lived MCP server process and a short-lived CJS hook
|
|
44
|
+
// subprocess launched concurrently). Without a lock + re-read-before-write,
|
|
45
|
+
// whichever process flushes last silently erases whatever the other process
|
|
46
|
+
// added since its own load. mergeAndWriteStore() re-reads the current
|
|
47
|
+
// on-disk file immediately before writing, merges the caller's updates in by
|
|
48
|
+
// `id` (keeping whichever record has the newer `ts`), and writes atomically
|
|
49
|
+
// via tmp+rename, guarded by a short advisory lock.
|
|
50
|
+
|
|
51
|
+
const STORE_LOCK_FILE = STORE_FILE + '.lock';
|
|
52
|
+
const STORE_LOCK_STALE_MS = 10 * 1000; // single JSON write — 10s is generous
|
|
53
|
+
|
|
54
|
+
// Uses fs-helpers.cjs's claimLock/releaseLock: unlike an unlink-then-recreate
|
|
55
|
+
// stale-break, it reclaims via an atomic rename, so two processes that both
|
|
56
|
+
// decide the lock is stale can't both end up believing they hold it (a real
|
|
57
|
+
// TOCTOU race a previous unlink-then-create version of this had — best-effort
|
|
58
|
+
// either way, since a missed lock still merges via mergeAndWriteStore's
|
|
59
|
+
// re-read, this just narrows the window further).
|
|
60
|
+
function claimStoreLock() {
|
|
61
|
+
return claimLock(STORE_LOCK_FILE, STORE_LOCK_STALE_MS);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function releaseStoreLock() {
|
|
65
|
+
releaseLock(STORE_LOCK_FILE);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Merge `updates` (new or changed entries) into the on-disk store and write
|
|
70
|
+
* the result atomically. Re-reads STORE_FILE immediately before writing so a
|
|
71
|
+
* concurrent flush from another process is merged, not clobbered. When both
|
|
72
|
+
* sides have an entry with the same `id`, the one with the newer `ts` wins.
|
|
73
|
+
*/
|
|
74
|
+
function mergeAndWriteStore(updates, capTo) {
|
|
75
|
+
var acquired = claimStoreLock();
|
|
76
|
+
if (!acquired) acquired = claimStoreLock(); // one retry — lock is short-lived
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
var diskRaw = safeReadJson(STORE_FILE);
|
|
80
|
+
var diskEntries = Array.isArray(diskRaw) ? diskRaw : [];
|
|
81
|
+
var merged = new Map();
|
|
82
|
+
for (var i = 0; i < diskEntries.length; i++) {
|
|
83
|
+
var d = diskEntries[i];
|
|
84
|
+
if (d && d.id) merged.set(String(d.id), d);
|
|
85
|
+
}
|
|
86
|
+
for (var j = 0; j < (updates || []).length; j++) {
|
|
87
|
+
var u = updates[j];
|
|
88
|
+
if (!u || !u.id) continue;
|
|
89
|
+
var key = String(u.id);
|
|
90
|
+
var existing = merged.get(key);
|
|
91
|
+
if (!existing || (u.ts || 0) >= (existing.ts || 0)) {
|
|
92
|
+
merged.set(key, u);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
var result = Array.from(merged.values());
|
|
96
|
+
if (capTo && result.length > capTo) {
|
|
97
|
+
result.sort(function(a, b) { return (b.ts || 0) - (a.ts || 0); });
|
|
98
|
+
result = result.slice(0, capTo);
|
|
99
|
+
}
|
|
100
|
+
var tmpPath = STORE_FILE + '.' + process.pid + '.tmp';
|
|
101
|
+
fs.writeFileSync(tmpPath, JSON.stringify(result, null, 2), 'utf-8');
|
|
102
|
+
fs.renameSync(tmpPath, STORE_FILE);
|
|
103
|
+
return result;
|
|
104
|
+
} finally {
|
|
105
|
+
if (acquired) releaseStoreLock();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
38
109
|
function safeReadJson(filePath) {
|
|
39
110
|
try {
|
|
40
111
|
if (!fs.existsSync(filePath)) return null;
|
|
@@ -274,7 +345,11 @@ function consolidate() {
|
|
|
274
345
|
}
|
|
275
346
|
|
|
276
347
|
if (newStoreEntries.length > 0) {
|
|
277
|
-
//
|
|
348
|
+
// Decide which of newStoreEntries are worth adding, based on a read of
|
|
349
|
+
// the store as of "now". The actual write below re-reads immediately
|
|
350
|
+
// before flushing (mergeAndWriteStore), so a concurrent process's writes
|
|
351
|
+
// in between are merged rather than clobbered — this initial read is
|
|
352
|
+
// only used for the near-duplicate decision, not as the write's base.
|
|
278
353
|
var existing = safeReadJson(STORE_FILE);
|
|
279
354
|
var store = Array.isArray(existing) ? existing : [];
|
|
280
355
|
var existingIds = new Set(store.map(function(e) { return e && e.id; }));
|
|
@@ -283,25 +358,23 @@ function consolidate() {
|
|
|
283
358
|
var fileSetKey = function(e) { return (e.files || []).slice().sort().join('|'); };
|
|
284
359
|
var seenSummaries = new Set(store.map(function(e) { return e && e.summary; }));
|
|
285
360
|
var seenFileSets = new Set(store.filter(function(e) { return e && Array.isArray(e.files) && e.files.length > 0; }).map(fileSetKey));
|
|
361
|
+
var toAdd = [];
|
|
286
362
|
var addedCount = 0;
|
|
287
363
|
for (var j = 0; j < newStoreEntries.length; j++) {
|
|
288
364
|
var ne = newStoreEntries[j];
|
|
289
365
|
if (existingIds.has(ne.id)) continue;
|
|
290
366
|
if (seenSummaries.has(ne.summary)) continue;
|
|
291
367
|
if (ne.files && ne.files.length > 0 && seenFileSets.has(fileSetKey(ne))) continue;
|
|
292
|
-
|
|
368
|
+
toAdd.push(ne);
|
|
293
369
|
seenSummaries.add(ne.summary);
|
|
294
370
|
if (ne.files && ne.files.length > 0) seenFileSets.add(fileSetKey(ne));
|
|
295
371
|
addedCount++;
|
|
296
372
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
373
|
+
if (toAdd.length > 0) {
|
|
374
|
+
try {
|
|
375
|
+
mergeAndWriteStore(toAdd, 200);
|
|
376
|
+
} catch (_) {}
|
|
301
377
|
}
|
|
302
|
-
try {
|
|
303
|
-
fs.writeFileSync(STORE_FILE, JSON.stringify(store, null, 2), 'utf-8');
|
|
304
|
-
} catch (_) {}
|
|
305
378
|
count += addedCount;
|
|
306
379
|
}
|
|
307
380
|
|
|
@@ -395,19 +468,20 @@ function bootstrapFromDb(db) {
|
|
|
395
468
|
if (!db || _entries.length >= 5) return 0;
|
|
396
469
|
try {
|
|
397
470
|
var hubs = db.prepare(
|
|
398
|
-
"SELECT n.name, n.label, n.file, COUNT(e.id) AS deg " +
|
|
399
|
-
"FROM nodes n JOIN edges e ON (e.
|
|
400
|
-
"WHERE n.label IN ('File','Function','Class') AND n.
|
|
471
|
+
"SELECT n.name, n.label, n.file_path AS file, COUNT(e.id) AS deg " +
|
|
472
|
+
"FROM nodes n JOIN edges e ON (e.source_id = n.id OR e.target_id = n.id) " +
|
|
473
|
+
"WHERE n.label IN ('File','Function','Class') AND n.file_path NOT LIKE '%node_modules%' AND n.file_path NOT LIKE '%dist/%' " +
|
|
401
474
|
"GROUP BY n.id ORDER BY deg DESC LIMIT 10"
|
|
402
475
|
).all();
|
|
403
476
|
if (hubs.length === 0) return 0;
|
|
404
477
|
var existingIds = new Set(_entries.map(function(e) { return e.id; }));
|
|
478
|
+
var newHubEntries = [];
|
|
405
479
|
var added = 0;
|
|
406
480
|
for (var hi = 0; hi < hubs.length; hi++) {
|
|
407
481
|
var h = hubs[hi];
|
|
408
482
|
var bId = 'bootstrap-hub-' + hi;
|
|
409
483
|
if (existingIds.has(bId)) continue;
|
|
410
|
-
|
|
484
|
+
var hubEntry = {
|
|
411
485
|
id: bId,
|
|
412
486
|
type: 'hub',
|
|
413
487
|
content: h.name + ' (' + h.label + ') — ' + (h.file || '').replace(CWD + '/', '') + ' (' + h.deg + ' connections)',
|
|
@@ -415,12 +489,17 @@ function bootstrapFromDb(db) {
|
|
|
415
489
|
confidence: 0.4,
|
|
416
490
|
files: h.file ? [h.file] : [],
|
|
417
491
|
ts: Date.now(),
|
|
418
|
-
}
|
|
492
|
+
};
|
|
493
|
+
_entries.push(hubEntry);
|
|
494
|
+
newHubEntries.push(hubEntry);
|
|
419
495
|
added++;
|
|
420
496
|
}
|
|
421
497
|
if (added > 0) {
|
|
422
498
|
ensureDataDir();
|
|
423
|
-
|
|
499
|
+
// mergeAndWriteStore re-reads STORE_FILE immediately before writing, so
|
|
500
|
+
// this only ever adds these hub entries into whatever is currently on
|
|
501
|
+
// disk — it never overwrites entries a concurrent process added.
|
|
502
|
+
try { mergeAndWriteStore(newHubEntries, 200); } catch (_) {}
|
|
424
503
|
}
|
|
425
504
|
return added;
|
|
426
505
|
} catch (_) { return 0; }
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const fs = require('fs');
|
|
12
|
+
const { atomicWriteFileSync } = require('./utils/fs-helpers.cjs');
|
|
12
13
|
|
|
13
14
|
const CWD = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
14
15
|
|
|
@@ -54,7 +55,11 @@ function readCurrent() {
|
|
|
54
55
|
function writeCurrent(data) {
|
|
55
56
|
ensureDir();
|
|
56
57
|
try {
|
|
57
|
-
|
|
58
|
+
// P2-21: tmp+rename — current.json is read+written by metric()/update()
|
|
59
|
+
// from multiple concurrent hook processes (pre-task, post-edit, etc. can
|
|
60
|
+
// all fire in one batched tool-call message); a plain writeFileSync lets
|
|
61
|
+
// a concurrent reader observe a partially-written file.
|
|
62
|
+
atomicWriteFileSync(CURRENT_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
58
63
|
} catch (_) {}
|
|
59
64
|
}
|
|
60
65
|
|
|
@@ -18,6 +18,7 @@ const fs = require('fs');
|
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const { execSync, spawnSync } = require('child_process');
|
|
20
20
|
const os = require('os');
|
|
21
|
+
const { cleanEntries } = require('./utils/fs-helpers.cjs');
|
|
21
22
|
|
|
22
23
|
// Configuration
|
|
23
24
|
const CONFIG = {
|
|
@@ -197,8 +198,7 @@ function getModelFromSessionJSONL() {
|
|
|
197
198
|
if (!fs.existsSync(projectsDir)) return null;
|
|
198
199
|
|
|
199
200
|
// Most recently modified JSONL = current (or latest) session
|
|
200
|
-
const files =
|
|
201
|
-
.filter(f => f.endsWith('.jsonl'))
|
|
201
|
+
const files = cleanEntries(projectsDir, f => f.endsWith('.jsonl'))
|
|
202
202
|
.map(f => ({ f, mt: (() => { try { return fs.statSync(path.join(projectsDir, f)).mtimeMs; } catch { return 0; } })() }))
|
|
203
203
|
.sort((a, b) => b.mt - a.mt);
|
|
204
204
|
if (files.length === 0) return null;
|
|
@@ -303,7 +303,7 @@ function getLearningStats() {
|
|
|
303
303
|
try {
|
|
304
304
|
const sessDir = path.join(CWD, '.claude', 'sessions');
|
|
305
305
|
if (fs.existsSync(sessDir)) {
|
|
306
|
-
sessions =
|
|
306
|
+
sessions = cleanEntries(sessDir, f => f.endsWith('.json')).length;
|
|
307
307
|
}
|
|
308
308
|
} catch { /* ignore */ }
|
|
309
309
|
|
|
@@ -357,7 +357,7 @@ function getSecurityStatus() {
|
|
|
357
357
|
try {
|
|
358
358
|
const scanDir = path.join(CWD, '.claude', 'security-scans');
|
|
359
359
|
if (fs.existsSync(scanDir)) {
|
|
360
|
-
scanCount =
|
|
360
|
+
scanCount = cleanEntries(scanDir, f => f.endsWith('.json')).length;
|
|
361
361
|
}
|
|
362
362
|
} catch { /* ignore */ }
|
|
363
363
|
|
|
@@ -379,7 +379,7 @@ function getSwarmStatus() {
|
|
|
379
379
|
const regDir = path.join(CWD, '.monomind', 'agents', 'registrations');
|
|
380
380
|
if (fs.existsSync(regDir)) {
|
|
381
381
|
try {
|
|
382
|
-
const files =
|
|
382
|
+
const files = cleanEntries(regDir, f => f.endsWith('.json'));
|
|
383
383
|
const liveCount = files.filter(f => {
|
|
384
384
|
try {
|
|
385
385
|
return (now - fs.statSync(path.join(regDir, f)).mtimeMs) < agentRegTtlMs;
|
|
@@ -488,7 +488,7 @@ function getADRStatus() {
|
|
|
488
488
|
for (const adrPath of adrPaths) {
|
|
489
489
|
try {
|
|
490
490
|
if (fs.existsSync(adrPath)) {
|
|
491
|
-
const files =
|
|
491
|
+
const files = cleanEntries(adrPath, f =>
|
|
492
492
|
f.endsWith('.md') && (f.startsWith('ADR-') || f.startsWith('adr-') || /^\d{4}-/.test(f))
|
|
493
493
|
);
|
|
494
494
|
// Report actual count — don't guess compliance without reading files
|
|
@@ -523,7 +523,7 @@ function getHooksStatus() {
|
|
|
523
523
|
try {
|
|
524
524
|
const hooksDir = path.join(CWD, '.claude', 'hooks');
|
|
525
525
|
if (fs.existsSync(hooksDir)) {
|
|
526
|
-
const hookFiles =
|
|
526
|
+
const hookFiles = cleanEntries(hooksDir, f => f.endsWith('.js') || f.endsWith('.sh')).length;
|
|
527
527
|
total = Math.max(total, hookFiles);
|
|
528
528
|
enabled = Math.max(enabled, hookFiles);
|
|
529
529
|
}
|
|
@@ -870,7 +870,7 @@ function getLoopStatus() {
|
|
|
870
870
|
const now = Date.now();
|
|
871
871
|
const loops = [];
|
|
872
872
|
try {
|
|
873
|
-
const files =
|
|
873
|
+
const files = cleanEntries(loopsDir, f =>
|
|
874
874
|
f.endsWith('.json') && !f.includes('-hil') && !f.endsWith('.stop'));
|
|
875
875
|
for (const f of files) {
|
|
876
876
|
const d = readJSON(path.join(loopsDir, f));
|
|
@@ -895,7 +895,7 @@ function getHILPending() {
|
|
|
895
895
|
if (!fs.existsSync(loopsDir)) return { pending: 0 };
|
|
896
896
|
let pending = 0;
|
|
897
897
|
try {
|
|
898
|
-
const files =
|
|
898
|
+
const files = cleanEntries(loopsDir, f => f.endsWith('-hil.md'));
|
|
899
899
|
for (const f of files) {
|
|
900
900
|
try {
|
|
901
901
|
const txt = fs.readFileSync(path.join(loopsDir, f), 'utf-8');
|
|
@@ -932,7 +932,7 @@ function getActiveOrgs() {
|
|
|
932
932
|
const runsDir = path.join(orgsDir, orgName, 'runs');
|
|
933
933
|
if (!fs.existsSync(runsDir)) continue;
|
|
934
934
|
try {
|
|
935
|
-
const files =
|
|
935
|
+
const files = cleanEntries(runsDir, f => f.endsWith('.jsonl'));
|
|
936
936
|
if (!files.length) continue;
|
|
937
937
|
files.sort();
|
|
938
938
|
const latest = files[files.length - 1];
|