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
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const fs = require('fs');
|
|
7
|
+
const { purgeStaleRegistrations } = require('../utils/agent-registrations.cjs');
|
|
8
|
+
const { cleanEntries, atomicWriteFileSync, withLock } = require('../utils/fs-helpers.cjs');
|
|
7
9
|
|
|
8
10
|
module.exports = {
|
|
9
11
|
handle: async function(hCtx) {
|
|
@@ -18,41 +20,65 @@ module.exports = {
|
|
|
18
20
|
fs.mkdirSync(regDir, { recursive: true });
|
|
19
21
|
const id = Date.now() + '-' + Math.random().toString(36).slice(2, 6);
|
|
20
22
|
const regFile = path.join(regDir, 'agent-' + id + '.json');
|
|
23
|
+
// P3-15: also stamp the agent's type/slug so handlePostTask (task-handler.cjs)
|
|
24
|
+
// can correlate a completion event back to THIS registration instead of
|
|
25
|
+
// blindly popping the oldest file — the registration previously carried no
|
|
26
|
+
// identity beyond a random id, so post-task had nothing to match against.
|
|
27
|
+
// Same field precedence as the last-dispatch.json write below, computed
|
|
28
|
+
// early so both writers agree on the agent's identity.
|
|
29
|
+
const MAX_TYPE_LEN = 128;
|
|
30
|
+
const agentType = String(hookInput.subagent_type || hookInput.agentType || hookInput.agent_type || hookInput.agentSlug || 'unknown').slice(0, MAX_TYPE_LEN);
|
|
21
31
|
fs.writeFileSync(regFile, JSON.stringify({
|
|
22
32
|
agentId: id,
|
|
33
|
+
agentType: agentType,
|
|
23
34
|
startedAt: new Date().toISOString(),
|
|
24
35
|
pid: process.pid,
|
|
25
36
|
}));
|
|
37
|
+
// Purge stragglers older than 30 min on every agent-start too — not just
|
|
38
|
+
// on TeammateIdle/TaskCompleted (task-handler.cjs) — since sessions that
|
|
39
|
+
// never emit those events would otherwise leak registrations forever.
|
|
40
|
+
const activeAfterPurge = purgeStaleRegistrations(regDir);
|
|
26
41
|
// Refresh swarm-activity.json within the 5-min staleness window.
|
|
27
42
|
const activityDir = path.join(CWD, '.monomind', 'metrics');
|
|
28
43
|
fs.mkdirSync(activityDir, { recursive: true });
|
|
29
44
|
const activityPath = path.join(activityDir, 'swarm-activity.json');
|
|
30
45
|
const MAX_AGENTS = 1000;
|
|
31
|
-
const active = Math.min(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
const active = Math.min(
|
|
47
|
+
activeAfterPurge != null ? activeAfterPurge : cleanEntries(regDir, f => f.endsWith('.json')).length,
|
|
48
|
+
MAX_AGENTS
|
|
49
|
+
);
|
|
50
|
+
// P2-21: swarm-activity.json is written by both this handler and
|
|
51
|
+
// task-handler.cjs's handlePostTask, concurrently (Claude Code fires
|
|
52
|
+
// hook events for parallel tool calls in one message). The old
|
|
53
|
+
// read-prevLastActive-then-write was a lost-update race — two
|
|
54
|
+
// processes could both read the same prevLastActive before either
|
|
55
|
+
// wrote, so whichever wrote last silently erased the other's peak.
|
|
56
|
+
// Share one lock file with task-handler.cjs so the read-merge-write
|
|
57
|
+
// cycle is a single critical section across both writers.
|
|
58
|
+
withLock(activityPath + '.lock', function() {
|
|
59
|
+
// Preserve lastActive (peak) so statusline shows non-zero after completion.
|
|
60
|
+
let prevLastActive = 0;
|
|
61
|
+
try {
|
|
62
|
+
const MAX_ACTIVITY = 64 * 1024; // 64 KiB
|
|
63
|
+
var actStat = fs.statSync(activityPath);
|
|
64
|
+
if (actStat.size <= MAX_ACTIVITY) {
|
|
65
|
+
prevLastActive = (JSON.parse(fs.readFileSync(activityPath, 'utf-8'))?.swarm?.lastActive) || 0;
|
|
66
|
+
}
|
|
67
|
+
} catch { /* ignore */ }
|
|
68
|
+
atomicWriteFileSync(activityPath, JSON.stringify({
|
|
69
|
+
timestamp: new Date().toISOString(),
|
|
70
|
+
swarm: {
|
|
71
|
+
active: active > 0,
|
|
72
|
+
agent_count: active,
|
|
73
|
+
coordination_active: active > 0,
|
|
74
|
+
lastActive: Math.max(active, prevLastActive),
|
|
75
|
+
},
|
|
76
|
+
}));
|
|
77
|
+
}, 5000);
|
|
50
78
|
|
|
51
79
|
// Write last-dispatch.json so the route handler can suppress redundant
|
|
52
80
|
// suggestions on the next turn when the same agent type is recommended.
|
|
53
|
-
const MAX_TYPE_LEN = 128;
|
|
54
81
|
const MAX_DESC_LEN = 500;
|
|
55
|
-
const agentType = String(hookInput.subagent_type || hookInput.agentType || hookInput.agent_type || hookInput.agentSlug || 'unknown').slice(0, MAX_TYPE_LEN);
|
|
56
82
|
const agentDesc = String(hookInput.description || hookInput.prompt_description || '').slice(0, MAX_DESC_LEN);
|
|
57
83
|
fs.writeFileSync(
|
|
58
84
|
path.join(CWD, '.monomind', 'last-dispatch.json'),
|
|
@@ -87,13 +113,19 @@ module.exports = {
|
|
|
87
113
|
if (_hooksModule && _hooksModule.executeHooks && _hooksModule.HookEvent) {
|
|
88
114
|
var agentTypeBridge = String(hookInput.subagent_type || hookInput.agentType || hookInput.agent_type || hookInput.agentSlug || 'unknown');
|
|
89
115
|
var agentDescBridge = String(hookInput.description || hookInput.prompt_description || '');
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
116
|
+
// Must be awaited (bounded by runWithTimeout, 1500ms) — hook-handler.cjs's
|
|
117
|
+
// main().finally(() => process.exit(...)) reaps any unawaited fire-and-forget
|
|
118
|
+
// promise before it does real I/O. See session-restore-handler.cjs's
|
|
119
|
+
// SessionStart bridge for the reference pattern this mirrors.
|
|
120
|
+
await hCtx.runWithTimeout(function() {
|
|
121
|
+
return _hooksModule.executeHooks(_hooksModule.HookEvent.AgentSpawn, {
|
|
122
|
+
agent: {
|
|
123
|
+
id: hookInput.agentId || hookInput.agent_id || (agentTypeBridge + '-' + Date.now()),
|
|
124
|
+
type: agentTypeBridge,
|
|
125
|
+
description: agentDescBridge.slice(0, 500),
|
|
126
|
+
},
|
|
127
|
+
}, { continueOnError: true, timeout: 1500 });
|
|
128
|
+
}, '@monomind/hooks.AgentSpawn');
|
|
97
129
|
}
|
|
98
130
|
} catch (e) { /* non-fatal */ }
|
|
99
131
|
},
|
|
@@ -21,9 +21,51 @@ const fs = require('fs');
|
|
|
21
21
|
const path = require('path');
|
|
22
22
|
const http = require('http');
|
|
23
23
|
const os = require('os');
|
|
24
|
+
const crypto = require('crypto');
|
|
25
|
+
const { appendJsonlWithRotation } = require('../utils/fs-helpers.cjs');
|
|
24
26
|
|
|
25
27
|
const CWD = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
26
28
|
const CAPTURE_DIR = path.join(CWD, '.monomind', 'capture');
|
|
29
|
+
const SNAP_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour — orphaned snapshots are cleaned up this old
|
|
30
|
+
|
|
31
|
+
// P2-23: derive a stable key for a subagent invocation from whatever
|
|
32
|
+
// identifying info Claude Code actually gives us in the hook payload.
|
|
33
|
+
// `transcript_path` is the one field that's both present on SubagentStart
|
|
34
|
+
// and SubagentStop AND unique per-subagent (each subagent gets its own
|
|
35
|
+
// transcript file, distinct from the parent session's) — session_id alone
|
|
36
|
+
// is shared by every concurrently-running subagent in the same team, so it
|
|
37
|
+
// can't disambiguate which stop event belongs to which start.
|
|
38
|
+
function subagentKey(hookInput) {
|
|
39
|
+
var raw = hookInput && (hookInput.transcript_path || hookInput.transcriptPath);
|
|
40
|
+
if (!raw) return null;
|
|
41
|
+
return crypto.createHash('md5').update(String(raw)).digest('hex').slice(0, 16);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Determine subagent success from its result summary — errors/failures/
|
|
45
|
+
// exceptions indicate failure. Shared by the Monograph `success` column and
|
|
46
|
+
// the intelligence.feedback() call below, so both use one definition.
|
|
47
|
+
function deriveSubagentSuccess(summary) {
|
|
48
|
+
if (!summary) return true;
|
|
49
|
+
var sumLower = summary.toLowerCase();
|
|
50
|
+
if (sumLower.includes('error') || sumLower.includes('failed') || sumLower.includes('exception') || sumLower.includes('fatal')) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Delete snap-*.json files older than SNAP_MAX_AGE_MS — orphaned when a
|
|
57
|
+
// SubagentStop never arrived (crash, forced-stop) or was matched to a
|
|
58
|
+
// different snapshot before keyed matching existed.
|
|
59
|
+
function cleanupStaleSnaps() {
|
|
60
|
+
try {
|
|
61
|
+
var files = fs.readdirSync(CAPTURE_DIR).filter(f => f.startsWith('snap-') && f.endsWith('.json') && !f.startsWith('._'));
|
|
62
|
+
var now = Date.now();
|
|
63
|
+
for (var i = 0; i < files.length; i++) {
|
|
64
|
+
var p = path.join(CAPTURE_DIR, files[i]);
|
|
65
|
+
try { if (now - fs.statSync(p).mtimeMs > SNAP_MAX_AGE_MS) fs.unlinkSync(p); } catch { /* ignore */ }
|
|
66
|
+
}
|
|
67
|
+
} catch { /* CAPTURE_DIR may not exist yet */ }
|
|
68
|
+
}
|
|
27
69
|
|
|
28
70
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
29
71
|
|
|
@@ -46,7 +88,7 @@ function snapshotJSONLFiles() {
|
|
|
46
88
|
if (!fs.existsSync(claudeDir)) return [];
|
|
47
89
|
try {
|
|
48
90
|
return fs.readdirSync(claudeDir)
|
|
49
|
-
.filter(f => f.endsWith('.jsonl'))
|
|
91
|
+
.filter(f => f.endsWith('.jsonl') && !f.startsWith('._'))
|
|
50
92
|
.map(f => {
|
|
51
93
|
try { return { name: f, size: fs.statSync(path.join(claudeDir, f)).size }; }
|
|
52
94
|
catch { return { name: f, size: 0 }; }
|
|
@@ -170,6 +212,7 @@ function emitEvent(event) {
|
|
|
170
212
|
|
|
171
213
|
async function handleSubagentStart(hookInput) {
|
|
172
214
|
fs.mkdirSync(CAPTURE_DIR, { recursive: true });
|
|
215
|
+
cleanupStaleSnaps();
|
|
173
216
|
|
|
174
217
|
const snapshot = snapshotJSONLFiles();
|
|
175
218
|
const agentType = String(
|
|
@@ -190,10 +233,19 @@ async function handleSubagentStart(hookInput) {
|
|
|
190
233
|
}
|
|
191
234
|
const activeSess = getActiveSession(activeRun);
|
|
192
235
|
|
|
193
|
-
//
|
|
236
|
+
// P2-23: key the snapshot filename by transcript_path when available so
|
|
237
|
+
// SubagentStop can look up the EXACT matching snapshot instead of
|
|
238
|
+
// FIFO-popping the lexicographically-oldest one — under real concurrency
|
|
239
|
+
// (multiple subagents stopping in close succession) FIFO pop can match a
|
|
240
|
+
// stop event to the wrong subagent's snapshot. Falls back to the old
|
|
241
|
+
// timestamp+random naming when transcript_path isn't present in the
|
|
242
|
+
// payload, so SubagentStop's FIFO fallback path still has something to
|
|
243
|
+
// match against.
|
|
244
|
+
const subKey = subagentKey(hookInput);
|
|
194
245
|
const snapFile = path.join(
|
|
195
246
|
CAPTURE_DIR,
|
|
196
|
-
|
|
247
|
+
subKey ? 'snap-' + subKey + '.json'
|
|
248
|
+
: 'snap-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6) + '.json'
|
|
197
249
|
);
|
|
198
250
|
const leanModePath = path.join(process.env.CLAUDE_PROJECT_DIR || process.cwd(), '.monomind/state/monolean-mode');
|
|
199
251
|
let leanMode = null;
|
|
@@ -235,18 +287,31 @@ async function handleSubagentStart(hookInput) {
|
|
|
235
287
|
|
|
236
288
|
async function handleSubagentStop(hookInput) {
|
|
237
289
|
fs.mkdirSync(CAPTURE_DIR, { recursive: true });
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
290
|
+
cleanupStaleSnaps();
|
|
291
|
+
|
|
292
|
+
// P2-23: prefer the exact keyed snapshot (see subagentKey/handleSubagentStart)
|
|
293
|
+
// over FIFO-popping the oldest snap-*.json — FIFO misattributes the stop
|
|
294
|
+
// event to the wrong subagent whenever multiple subagents stop in close
|
|
295
|
+
// succession. Fall back to FIFO only when this event's payload carries no
|
|
296
|
+
// transcript_path (older Claude Code versions / unexpected payload shape).
|
|
297
|
+
const subKey = subagentKey(hookInput);
|
|
298
|
+
let snapPath = null;
|
|
299
|
+
if (subKey) {
|
|
300
|
+
const keyedPath = path.join(CAPTURE_DIR, 'snap-' + subKey + '.json');
|
|
301
|
+
if (fs.existsSync(keyedPath)) snapPath = keyedPath;
|
|
302
|
+
}
|
|
303
|
+
if (!snapPath) {
|
|
304
|
+
const snapFiles = fs.readdirSync(CAPTURE_DIR)
|
|
305
|
+
.filter(f => f.startsWith('snap-') && f.endsWith('.json') && !f.startsWith('._'))
|
|
306
|
+
.sort(); // lexicographic = timestamp order (FIFO fallback)
|
|
307
|
+
if (snapFiles.length === 0) {
|
|
308
|
+
console.log('[CAPTURE:stop] No snapshot to diff — skipping');
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
snapPath = path.join(CAPTURE_DIR, snapFiles[0]);
|
|
312
|
+
console.log('[CAPTURE:stop] no transcript_path/keyed snapshot — falling back to FIFO match');
|
|
247
313
|
}
|
|
248
314
|
|
|
249
|
-
const snapPath = path.join(CAPTURE_DIR, snapFiles[0]);
|
|
250
315
|
let snap;
|
|
251
316
|
try { snap = JSON.parse(fs.readFileSync(snapPath, 'utf8')); } catch {
|
|
252
317
|
try { fs.unlinkSync(snapPath); } catch {}
|
|
@@ -292,13 +357,26 @@ async function handleSubagentStop(hookInput) {
|
|
|
292
357
|
+ (session ? ' · sess=' + session : '')
|
|
293
358
|
+ (org ? ' · ' + org + '/' + runId : ''));
|
|
294
359
|
|
|
360
|
+
// Feed the real per-subagent success/failure signal into the same
|
|
361
|
+
// intelligence-outcomes.jsonl that session-handler.cjs's SessionEnd
|
|
362
|
+
// heuristic reads (via outcomesSignal) to veto a commit-based "success".
|
|
363
|
+
// Without this, that veto path never fires — SubagentStop is the only
|
|
364
|
+
// remaining event that carries a genuine failure signal, since post-task
|
|
365
|
+
// (TeammateIdle/TaskCompleted) is dead code (those aren't valid Claude
|
|
366
|
+
// Code hook events and are stripped from settings.json on init).
|
|
367
|
+
try {
|
|
368
|
+
require('../intelligence.cjs').feedback(deriveSubagentSuccess(summary));
|
|
369
|
+
} catch (e) { /* non-fatal — feedback recording must never block subagent-stop */ }
|
|
370
|
+
|
|
295
371
|
if (!org && !session) {
|
|
296
372
|
// No active org or session — log to general capture file only
|
|
373
|
+
// P2-26: rotate — was a pure appendFileSync with no cap, unlike
|
|
374
|
+
// intelligence-outcomes.jsonl (500-line cap).
|
|
297
375
|
const genLog = path.join(CAPTURE_DIR, 'unattributed.jsonl');
|
|
298
|
-
|
|
376
|
+
appendJsonlWithRotation(genLog, JSON.stringify({
|
|
299
377
|
ts: Date.now(), agentType, tokens_in: totalTin, tokens_out: totalTout,
|
|
300
378
|
cost_usd: costUsd, capturedFiles, summary: summary.slice(0, 200),
|
|
301
|
-
})
|
|
379
|
+
}), 500);
|
|
302
380
|
return;
|
|
303
381
|
}
|
|
304
382
|
|
|
@@ -355,9 +433,11 @@ async function handleSubagentStop(hookInput) {
|
|
|
355
433
|
// ── Persist transcript reference to run directory ─────────────────────────
|
|
356
434
|
if (capturedFiles.length > 0 && org && runId) {
|
|
357
435
|
const runDir = path.join(CWD, '.monomind', 'orgs', org, 'runs');
|
|
358
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
359
436
|
const capLog = path.join(runDir, runId + '-captures.jsonl');
|
|
360
|
-
|
|
437
|
+
// P2-26: rotate — was a pure appendFileSync with no cap. A long-running
|
|
438
|
+
// org loop can spawn far more than 500 agents over its lifetime, so
|
|
439
|
+
// this uses the same 500-line cap convention as the other JSONL logs.
|
|
440
|
+
appendJsonlWithRotation(capLog, JSON.stringify({
|
|
361
441
|
type: 'agent:capture',
|
|
362
442
|
org, runId, session,
|
|
363
443
|
agentType,
|
|
@@ -368,7 +448,7 @@ async function handleSubagentStop(hookInput) {
|
|
|
368
448
|
capturedFiles,
|
|
369
449
|
claudeProjectDir: claudeDir,
|
|
370
450
|
ts: Date.now(),
|
|
371
|
-
})
|
|
451
|
+
}), 500);
|
|
372
452
|
}
|
|
373
453
|
|
|
374
454
|
// ── Wire captured interaction into EpisodicStore ────
|
|
@@ -429,16 +509,7 @@ async function handleSubagentStop(hookInput) {
|
|
|
429
509
|
tokens_in: totalTin || 0,
|
|
430
510
|
tokens_out: totalTout || 0,
|
|
431
511
|
cost_usd: costUsd || 0,
|
|
432
|
-
success: (
|
|
433
|
-
// Determine success from result summary — errors/failures/exceptions indicate failure
|
|
434
|
-
if (summary) {
|
|
435
|
-
var sumLower = summary.toLowerCase();
|
|
436
|
-
if (sumLower.includes('error') || sumLower.includes('failed') || sumLower.includes('exception') || sumLower.includes('fatal')) {
|
|
437
|
-
return 0;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
return 1;
|
|
441
|
-
})(),
|
|
512
|
+
success: deriveSubagentSuccess(summary) ? 1 : 0,
|
|
442
513
|
duration_ms: snap.ts ? (Date.now() - snap.ts) : 0,
|
|
443
514
|
timestamp: Date.now(),
|
|
444
515
|
});
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const fs = require('fs');
|
|
8
|
+
const { claimLock } = require('../utils/fs-helpers.cjs');
|
|
8
9
|
|
|
9
10
|
module.exports = {
|
|
10
11
|
handle: async function(hCtx) {
|
|
@@ -84,12 +85,23 @@ module.exports = {
|
|
|
84
85
|
var codeExts = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|cs|cpp|c|rb|swift|php)$/i;
|
|
85
86
|
if (editedFile2 && codeExts.test(editedFile2)) {
|
|
86
87
|
var lockFile = path.join(CWD, '.monomind', 'graph', '.rebuild-lock');
|
|
87
|
-
var now = Date.now();
|
|
88
|
-
var lastBuild = 0;
|
|
89
|
-
try { var _lkSt = fs.statSync(lockFile); if (_lkSt.size <= 32) { lastBuild = parseInt(fs.readFileSync(lockFile, 'utf-8').trim(), 10) || 0; } } catch (e) {}
|
|
90
88
|
var COOLDOWN_MS = 5000; // 5-second debounce
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
// P3-17: on a fresh project (no full `monograph build` has ever run),
|
|
90
|
+
// .monomind/graph/ doesn't exist yet, and claimLock's writeFileSync
|
|
91
|
+
// below throws ENOENT — caught by this try/catch and silently
|
|
92
|
+
// swallowed, so the incremental rebuild (and blast-radius display)
|
|
93
|
+
// never fires until some other code path happens to create the dir.
|
|
94
|
+
try { fs.mkdirSync(path.dirname(lockFile), { recursive: true }); } catch (e) { /* non-fatal */ }
|
|
95
|
+
// P2-22: claim the cooldown lock atomically (wx-create) instead of
|
|
96
|
+
// statSync-then-writeFileSync — under real Claude Code concurrency a
|
|
97
|
+
// multi-file Edit in one message fires several PostToolUse hook
|
|
98
|
+
// processes near-simultaneously, and a plain read-check-write lets
|
|
99
|
+
// ALL of them read the same stale lastBuild timestamp before any of
|
|
100
|
+
// them writes a fresh one, so every one of them passes the cooldown
|
|
101
|
+
// check and spawns its own rebuild + ua-enrich child (see the
|
|
102
|
+
// 896-orphan-process incident fixed in control-start.cjs). claimLock
|
|
103
|
+
// mirrors that same TOCTOU-safe stale-lock-breaking pattern.
|
|
104
|
+
if (claimLock(lockFile, COOLDOWN_MS)) {
|
|
93
105
|
var { spawn: spawnRebuild } = require('child_process');
|
|
94
106
|
var rebuildScript = "import { buildAsync } from '@monoes/monograph'; await buildAsync(" + JSON.stringify(CWD) + ");";
|
|
95
107
|
var graphDir = path.join(CWD, '.monomind', 'graph');
|
|
@@ -148,10 +160,16 @@ module.exports = {
|
|
|
148
160
|
try {
|
|
149
161
|
var editFileBridge = hookInput.file_path || toolInput.file_path
|
|
150
162
|
|| process.env.TOOL_INPUT_file_path || args[0] || '';
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
163
|
+
// Must be awaited (bounded by runWithTimeout, 1500ms) — hook-handler.cjs's
|
|
164
|
+
// main().finally(() => process.exit(...)) reaps any unawaited fire-and-forget
|
|
165
|
+
// promise before it does real I/O. See session-restore-handler.cjs's
|
|
166
|
+
// SessionStart bridge for the reference pattern this mirrors.
|
|
167
|
+
await hCtx.runWithTimeout(function() {
|
|
168
|
+
return _hooksModule.executeHooks(_hooksModule.HookEvent.PostEdit, {
|
|
169
|
+
file: { path: editFileBridge, operation: 'modify' },
|
|
170
|
+
duration: 0,
|
|
171
|
+
}, { continueOnError: true, timeout: 1500 });
|
|
172
|
+
}, '@monomind/hooks.PostEdit');
|
|
155
173
|
} catch (e) { /* non-fatal */ }
|
|
156
174
|
}
|
|
157
175
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @monomind/guidance package it mirrored has been removed.
|
|
7
7
|
*
|
|
8
8
|
* Gates enforced at runtime:
|
|
9
|
-
* pre-bash → destructive-ops (
|
|
9
|
+
* pre-bash → destructive-ops (hard block; no confirm-and-proceed path exists)
|
|
10
10
|
* pre-write → secrets (block)
|
|
11
11
|
*/
|
|
12
12
|
|
|
@@ -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,
|
|
@@ -55,7 +60,7 @@ function checkDestructive(command) {
|
|
|
55
60
|
return {
|
|
56
61
|
triggered: true,
|
|
57
62
|
matched: match[0],
|
|
58
|
-
reason: `Destructive operation
|
|
63
|
+
reason: `Destructive operation blocked: "${match[0]}". This hook always blocks (Claude Code's PreToolUse protocol has no confirm-and-proceed path) — there is no way to run this exact command after confirming. If it's genuinely intended, use a non-destructive equivalent instead (e.g. move the target aside, or scope the operation more narrowly).`,
|
|
59
64
|
};
|
|
60
65
|
}
|
|
61
66
|
}
|
|
@@ -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();
|
|
@@ -299,6 +299,35 @@ module.exports = {
|
|
|
299
299
|
console.log('[CODEBASE] Graph index ' + mapData.graphStaleness.commitsBehind + ' commits behind HEAD — run monograph build');
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
|
+
// Graph gate connectivity nudge — the pre-search/pre-bash gate
|
|
303
|
+
// (utils/monograph.cjs _graphGateShouldBlock) hard-blocks the first
|
|
304
|
+
// Grep/Glob/bash-grep-or-find call each session until a real
|
|
305
|
+
// monograph_query/monograph_suggest call fires. If that block was
|
|
306
|
+
// never followed by a real graph call, the monomind MCP server is
|
|
307
|
+
// most likely not connected this session (config present but
|
|
308
|
+
// unapproved/not started) — surface it once so the user can fix the
|
|
309
|
+
// actual cause instead of the gate silently degrading to a no-op.
|
|
310
|
+
var graphGateFile = path.join(CWD, '.monomind', 'graph-gate-state.json');
|
|
311
|
+
var mcpWarnFile = path.join(CWD, '.monomind', 'mcp-not-connected-warned.json');
|
|
312
|
+
if (fs.existsSync(graphGateFile) && fs.statSync(graphGateFile).size < 4096) {
|
|
313
|
+
var gateState = JSON.parse(fs.readFileSync(graphGateFile, 'utf-8'));
|
|
314
|
+
var gateSessId = String((hCtx.hookInput && (hCtx.hookInput.sessionId || hCtx.hookInput.session_id)) || '');
|
|
315
|
+
if (gateState && gateState.sessionId === gateSessId && gateState.blockedOnce && !gateState.queried) {
|
|
316
|
+
var alreadyWarnedMcp = false;
|
|
317
|
+
if (fs.existsSync(mcpWarnFile)) {
|
|
318
|
+
try {
|
|
319
|
+
var mcpWarnData = JSON.parse(fs.readFileSync(mcpWarnFile, 'utf-8'));
|
|
320
|
+
if (mcpWarnData && mcpWarnData.sessionId === gateSessId) alreadyWarnedMcp = true;
|
|
321
|
+
} catch (e) { /* corrupt — warn again to be safe */ }
|
|
322
|
+
}
|
|
323
|
+
if (!alreadyWarnedMcp) {
|
|
324
|
+
console.log('[MCP] The graph gate blocked a search but no monograph_query/monograph_suggest call followed — the monomind MCP server is likely not connected this session. Run `claude mcp add monomind -- npx monomind@latest mcp start` (then restart), or approve the .mcp.json trust prompt if one is pending.');
|
|
325
|
+
try {
|
|
326
|
+
fs.writeFileSync(mcpWarnFile, JSON.stringify({ sessionId: gateSessId, warnedAt: new Date().toISOString() }));
|
|
327
|
+
} catch (e) { /* non-fatal */ }
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
302
331
|
// Deep dive findings (god nodes, high-degree files from background analysis)
|
|
303
332
|
var deepdiveFile = path.join(metricsDir, 'deepdive.json');
|
|
304
333
|
if (fs.existsSync(deepdiveFile) && fs.statSync(deepdiveFile).size < 32768) {
|
|
@@ -327,15 +356,15 @@ module.exports = {
|
|
|
327
356
|
}
|
|
328
357
|
}
|
|
329
358
|
}
|
|
330
|
-
// Performance metrics (
|
|
359
|
+
// Performance metrics (optimize worker — a one-shot process, not a daemon)
|
|
331
360
|
var perfFile = path.join(metricsDir, 'performance.json');
|
|
332
361
|
if (fs.existsSync(perfFile) && fs.statSync(perfFile).size < 32768) {
|
|
333
362
|
var perfData = JSON.parse(fs.readFileSync(perfFile, 'utf-8'));
|
|
334
|
-
if (perfData && perfData.
|
|
335
|
-
var rssBytes = perfData.
|
|
363
|
+
if (perfData && perfData.workerProcessMemoryUsage) {
|
|
364
|
+
var rssBytes = perfData.workerProcessMemoryUsage.rss || 0;
|
|
336
365
|
var rssMB = Math.round(rssBytes / (1024 * 1024));
|
|
337
366
|
if (rssMB > 512) {
|
|
338
|
-
console.log('[PERF]
|
|
367
|
+
console.log('[PERF] optimize worker RSS ' + rssMB + 'MB (>512MB threshold) at last run — reflects that one-shot process, not a persistent daemon');
|
|
339
368
|
}
|
|
340
369
|
}
|
|
341
370
|
}
|