monomind 2.0.3 → 2.1.3
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/README.md +74 -92
- package/package.json +11 -7
- 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 +99 -28
- package/packages/@monomind/cli/.claude/helpers/handlers/edit-handler.cjs +27 -9
- package/packages/@monomind/cli/.claude/helpers/handlers/gates-handler.cjs +18 -9
- package/packages/@monomind/cli/.claude/helpers/handlers/loops-status-handler.cjs +1 -1
- package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +33 -4
- package/packages/@monomind/cli/.claude/helpers/handlers/session-handler.cjs +75 -45
- 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 +75 -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/monograph.cjs +73 -2
- package/packages/@monomind/cli/.claude/helpers/utils/telemetry.cjs +35 -19
- package/packages/@monomind/cli/README.md +74 -92
- package/packages/@monomind/cli/bin/cli.js +10 -1
- package/packages/@monomind/cli/dist/src/browser/workflow/store.d.ts +2 -2
- package/packages/@monomind/cli/dist/src/commands/agent-ops.js +9 -15
- package/packages/@monomind/cli/dist/src/commands/browse.d.ts +1 -1
- 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-env-checks.js +5 -0
- 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 +321 -127
- package/packages/@monomind/cli/dist/src/commands/platforms.d.ts +1 -1
- 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 +93 -55
- 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/auto-install.d.ts +8 -8
- package/packages/@monomind/cli/dist/src/mcp-tools/coherence/types.d.ts +31 -31
- 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 +56 -35
- 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 +88 -41
- package/packages/@monomind/cli/dist/src/mcp-tools/quality/coverage-analysis/prioritize-gaps.d.ts +36 -36
- package/packages/@monomind/cli/dist/src/mcp-tools/quality/security-compliance/detect-secrets.d.ts +4 -4
- 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/ewc-consolidation.d.ts +12 -0
- 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/memory/sona-optimizer.d.ts +12 -0
- package/packages/@monomind/cli/dist/src/orgrt/broker.d.ts +26 -0
- package/packages/@monomind/cli/dist/src/orgrt/broker.js +73 -0
- package/packages/@monomind/cli/dist/src/orgrt/bus.d.ts +5 -1
- package/packages/@monomind/cli/dist/src/orgrt/bus.js +5 -1
- package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +46 -0
- package/packages/@monomind/cli/dist/src/orgrt/daemon.js +201 -18
- package/packages/@monomind/cli/dist/src/orgrt/forwarder.d.ts +10 -5
- package/packages/@monomind/cli/dist/src/orgrt/forwarder.js +138 -8
- package/packages/@monomind/cli/dist/src/orgrt/inbox.d.ts +11 -0
- package/packages/@monomind/cli/dist/src/orgrt/inbox.js +56 -0
- package/packages/@monomind/cli/dist/src/orgrt/policy.d.ts +5 -1
- package/packages/@monomind/cli/dist/src/orgrt/policy.js +58 -5
- 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 +9 -0
- package/packages/@monomind/cli/dist/src/orgrt/server.js +39 -0
- package/packages/@monomind/cli/dist/src/orgrt/session.d.ts +10 -1
- package/packages/@monomind/cli/dist/src/orgrt/session.js +24 -1
- 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/orgrt/types.d.ts +52 -52
- package/packages/@monomind/cli/dist/src/output.d.ts +29 -29
- package/packages/@monomind/cli/dist/src/output.js +22 -7
- 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/types.d.ts +2 -2
- package/packages/@monomind/cli/dist/src/ui/dashboard.html +243 -49
- package/packages/@monomind/cli/dist/src/ui/orgs-files.js +192 -0
- package/packages/@monomind/cli/dist/src/ui/orgs.html +79 -11
- package/packages/@monomind/cli/dist/src/ui/server.mjs +346 -139
- 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 +15 -15
|
@@ -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];
|
|
@@ -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
|
}
|
|
@@ -9,6 +9,25 @@ const { _getRecentEdits } = require('./telemetry.cjs');
|
|
|
9
9
|
|
|
10
10
|
const CWD = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
11
11
|
|
|
12
|
+
// @monoes/monograph is "type":"module" with an exports map that has no
|
|
13
|
+
// "require" condition — a bare `require('@monoes/monograph')` (or
|
|
14
|
+
// require() of its package directory) always throws "No exports main
|
|
15
|
+
// defined", regardless of whether the package is actually installed.
|
|
16
|
+
// require()-ing the package's *resolved entry file* directly bypasses the
|
|
17
|
+
// exports-map restriction and works via Node's require(esm) support (this
|
|
18
|
+
// codebase targets Node 20+, which has it). Reads the entry path from the
|
|
19
|
+
// package's own package.json instead of hardcoding "dist/src/index.js" so
|
|
20
|
+
// this survives a future monograph dist-layout change.
|
|
21
|
+
function _resolvePkgEntryFile(pkgDir) {
|
|
22
|
+
try {
|
|
23
|
+
var pkg = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf-8'));
|
|
24
|
+
var entry = (pkg.exports && pkg.exports['.'] && (pkg.exports['.'].import || pkg.exports['.'].default)) || pkg.main;
|
|
25
|
+
if (!entry) return null;
|
|
26
|
+
var full = path.join(pkgDir, entry);
|
|
27
|
+
return fs.existsSync(full) ? full : null;
|
|
28
|
+
} catch (e) { return null; }
|
|
29
|
+
}
|
|
30
|
+
|
|
12
31
|
function _requireMonograph() {
|
|
13
32
|
var candidates = [
|
|
14
33
|
path.join(CWD, 'node_modules/.pnpm/node_modules/@monoes/monograph'),
|
|
@@ -16,9 +35,21 @@ function _requireMonograph() {
|
|
|
16
35
|
path.join(CWD, 'node_modules/@monoes/monograph'),
|
|
17
36
|
];
|
|
18
37
|
for (var i = 0; i < candidates.length; i++) {
|
|
19
|
-
|
|
38
|
+
var entry = fs.existsSync(candidates[i]) ? _resolvePkgEntryFile(candidates[i]) : null;
|
|
39
|
+
if (entry) { try { return require(entry); } catch (e) {} }
|
|
40
|
+
}
|
|
41
|
+
// Ancestor-directory search — the equivalent of bare `require('@monoes/
|
|
42
|
+
// monograph')`'s own node_modules walk, needed since that call form can
|
|
43
|
+
// never succeed against this package's exports map.
|
|
44
|
+
var dir = CWD;
|
|
45
|
+
for (;;) {
|
|
46
|
+
var pkgDir = path.join(dir, 'node_modules', '@monoes', 'monograph');
|
|
47
|
+
var entry = fs.existsSync(pkgDir) ? _resolvePkgEntryFile(pkgDir) : null;
|
|
48
|
+
if (entry) { try { return require(entry); } catch (e) {} break; }
|
|
49
|
+
var parent = path.dirname(dir);
|
|
50
|
+
if (parent === dir) break;
|
|
51
|
+
dir = parent;
|
|
20
52
|
}
|
|
21
|
-
try { return require('@monoes/monograph'); } catch(e) {}
|
|
22
53
|
return null;
|
|
23
54
|
}
|
|
24
55
|
|
|
@@ -257,6 +288,44 @@ function _recordGraphTelemetry(event) {
|
|
|
257
288
|
} catch (e) { /* non-fatal */ }
|
|
258
289
|
}
|
|
259
290
|
|
|
291
|
+
// ─── Graph gate ─────────────────────────────────────────────────────────────
|
|
292
|
+
// The pre-search/pre-bash heuristic assist above silently resolves Grep/Bash
|
|
293
|
+
// patterns against the graph and counts that as a "graph win" even when the
|
|
294
|
+
// agent never actually called monograph_query — so the graph-usage % can look
|
|
295
|
+
// healthy while zero real monograph_call events ever fire. This gate forces
|
|
296
|
+
// at least one real monograph_query/monograph_suggest call per session before
|
|
297
|
+
// Grep/Glob/bash grep|find are allowed, by hard-blocking (exitCode 2) the
|
|
298
|
+
// first such call each session. Capped at ONE block per session (never a
|
|
299
|
+
// second) so a subagent with no monograph MCP tool access can't deadlock.
|
|
300
|
+
function _graphGateStateFile() {
|
|
301
|
+
return path.join(CWD, '.monomind', 'graph-gate-state.json');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function _graphGateMarkQueried(sessionId) {
|
|
305
|
+
if (!sessionId) return;
|
|
306
|
+
try {
|
|
307
|
+
fs.mkdirSync(path.join(CWD, '.monomind'), { recursive: true });
|
|
308
|
+
fs.writeFileSync(_graphGateStateFile(), JSON.stringify({ sessionId: sessionId, queried: true }));
|
|
309
|
+
} catch (e) { /* non-fatal */ }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function _graphGateShouldBlock(sessionId) {
|
|
313
|
+
if (!sessionId || !_isGraphFresh()) return false;
|
|
314
|
+
var f = _graphGateStateFile();
|
|
315
|
+
var d = {};
|
|
316
|
+
try { d = JSON.parse(fs.readFileSync(f, 'utf-8')); } catch (e) {}
|
|
317
|
+
if (typeof d !== 'object' || d === null || d.sessionId !== sessionId) {
|
|
318
|
+
d = { sessionId: sessionId, queried: false, blockedOnce: false };
|
|
319
|
+
}
|
|
320
|
+
if (d.queried || d.blockedOnce) return false;
|
|
321
|
+
d.blockedOnce = true;
|
|
322
|
+
try {
|
|
323
|
+
fs.mkdirSync(path.join(CWD, '.monomind'), { recursive: true });
|
|
324
|
+
fs.writeFileSync(f, JSON.stringify(d));
|
|
325
|
+
} catch (e) { return false; }
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
|
|
260
329
|
function _injectCompactGraphMap() {
|
|
261
330
|
try {
|
|
262
331
|
var db = _openMonographDb();
|
|
@@ -476,5 +545,7 @@ module.exports = {
|
|
|
476
545
|
_injectCompactGraphMap,
|
|
477
546
|
_findAffectedTests,
|
|
478
547
|
_maybeRebuildMonograph,
|
|
548
|
+
_graphGateShouldBlock,
|
|
549
|
+
_graphGateMarkQueried,
|
|
479
550
|
injectGodNodesContext,
|
|
480
551
|
};
|
|
@@ -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
|
|