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.
Files changed (113) hide show
  1. package/README.md +74 -92
  2. package/package.json +11 -7
  3. package/packages/@monomind/cli/.claude/helpers/control-start.cjs +13 -20
  4. package/packages/@monomind/cli/.claude/helpers/event-logger.cjs +109 -6
  5. package/packages/@monomind/cli/.claude/helpers/graphify-freshen.cjs +19 -17
  6. package/packages/@monomind/cli/.claude/helpers/handlers/agent-start-handler.cjs +60 -28
  7. package/packages/@monomind/cli/.claude/helpers/handlers/capture-handler.cjs +99 -28
  8. package/packages/@monomind/cli/.claude/helpers/handlers/edit-handler.cjs +27 -9
  9. package/packages/@monomind/cli/.claude/helpers/handlers/gates-handler.cjs +18 -9
  10. package/packages/@monomind/cli/.claude/helpers/handlers/loops-status-handler.cjs +1 -1
  11. package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +33 -4
  12. package/packages/@monomind/cli/.claude/helpers/handlers/session-handler.cjs +75 -45
  13. package/packages/@monomind/cli/.claude/helpers/handlers/session-restore-handler.cjs +0 -12
  14. package/packages/@monomind/cli/.claude/helpers/handlers/task-handler.cjs +53 -22
  15. package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +75 -4
  16. package/packages/@monomind/cli/.claude/helpers/intelligence.cjs +94 -15
  17. package/packages/@monomind/cli/.claude/helpers/session.cjs +6 -1
  18. package/packages/@monomind/cli/.claude/helpers/statusline.cjs +10 -10
  19. package/packages/@monomind/cli/.claude/helpers/token-tracker.cjs +3 -3
  20. package/packages/@monomind/cli/.claude/helpers/utils/agent-registrations.cjs +41 -0
  21. package/packages/@monomind/cli/.claude/helpers/utils/fs-helpers.cjs +183 -0
  22. package/packages/@monomind/cli/.claude/helpers/utils/micro-agents.cjs +10 -6
  23. package/packages/@monomind/cli/.claude/helpers/utils/monograph.cjs +73 -2
  24. package/packages/@monomind/cli/.claude/helpers/utils/telemetry.cjs +35 -19
  25. package/packages/@monomind/cli/README.md +74 -92
  26. package/packages/@monomind/cli/bin/cli.js +10 -1
  27. package/packages/@monomind/cli/dist/src/browser/workflow/store.d.ts +2 -2
  28. package/packages/@monomind/cli/dist/src/commands/agent-ops.js +9 -15
  29. package/packages/@monomind/cli/dist/src/commands/browse.d.ts +1 -1
  30. package/packages/@monomind/cli/dist/src/commands/cleanup.js +20 -4
  31. package/packages/@monomind/cli/dist/src/commands/completions.js +66 -261
  32. package/packages/@monomind/cli/dist/src/commands/doctor-env-checks.js +5 -0
  33. package/packages/@monomind/cli/dist/src/commands/doctor.js +7 -2
  34. package/packages/@monomind/cli/dist/src/commands/hooks-workers.js +7 -0
  35. package/packages/@monomind/cli/dist/src/commands/index.js +3 -0
  36. package/packages/@monomind/cli/dist/src/commands/mcp.js +11 -6
  37. package/packages/@monomind/cli/dist/src/commands/memory-admin.js +12 -6
  38. package/packages/@monomind/cli/dist/src/commands/memory-transfer.js +6 -13
  39. package/packages/@monomind/cli/dist/src/commands/monograph.js +3 -3
  40. package/packages/@monomind/cli/dist/src/commands/org.d.ts +3 -1
  41. package/packages/@monomind/cli/dist/src/commands/org.js +321 -127
  42. package/packages/@monomind/cli/dist/src/commands/platforms.d.ts +1 -1
  43. package/packages/@monomind/cli/dist/src/commands/start.js +142 -37
  44. package/packages/@monomind/cli/dist/src/commands/tokens.js +21 -3
  45. package/packages/@monomind/cli/dist/src/config-adapter.js +37 -17
  46. package/packages/@monomind/cli/dist/src/index.js +64 -28
  47. package/packages/@monomind/cli/dist/src/init/claudemd-generator.js +44 -11
  48. package/packages/@monomind/cli/dist/src/init/executor.js +93 -55
  49. package/packages/@monomind/cli/dist/src/mcp-client.d.ts +8 -3
  50. package/packages/@monomind/cli/dist/src/mcp-client.js +37 -3
  51. package/packages/@monomind/cli/dist/src/mcp-server.js +8 -2
  52. package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.d.ts +20 -0
  53. package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.js +10 -5
  54. package/packages/@monomind/cli/dist/src/mcp-tools/auto-install.d.ts +8 -8
  55. package/packages/@monomind/cli/dist/src/mcp-tools/coherence/types.d.ts +31 -31
  56. package/packages/@monomind/cli/dist/src/mcp-tools/github-tools.js +30 -8
  57. package/packages/@monomind/cli/dist/src/mcp-tools/graphify-tools.js +11 -2
  58. package/packages/@monomind/cli/dist/src/mcp-tools/hive-mind-tools.js +56 -35
  59. package/packages/@monomind/cli/dist/src/mcp-tools/knowledge-tools.js +9 -1
  60. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-compat.js +56 -14
  61. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-tools.js +88 -41
  62. package/packages/@monomind/cli/dist/src/mcp-tools/quality/coverage-analysis/prioritize-gaps.d.ts +36 -36
  63. package/packages/@monomind/cli/dist/src/mcp-tools/quality/security-compliance/detect-secrets.d.ts +4 -4
  64. package/packages/@monomind/cli/dist/src/mcp-tools/swarm-tools.js +6 -4
  65. package/packages/@monomind/cli/dist/src/mcp-tools/system-tools.js +4 -4
  66. package/packages/@monomind/cli/dist/src/mcp-tools/types.d.ts +20 -0
  67. package/packages/@monomind/cli/dist/src/mcp-tools/types.js +36 -1
  68. package/packages/@monomind/cli/dist/src/memory/ewc-consolidation.d.ts +12 -0
  69. package/packages/@monomind/cli/dist/src/memory/hnsw-operations.d.ts +13 -1
  70. package/packages/@monomind/cli/dist/src/memory/hnsw-operations.js +66 -14
  71. package/packages/@monomind/cli/dist/src/memory/intelligence.js +99 -3
  72. package/packages/@monomind/cli/dist/src/memory/memory-bridge.d.ts +7 -0
  73. package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +20 -0
  74. package/packages/@monomind/cli/dist/src/memory/sona-optimizer.d.ts +12 -0
  75. package/packages/@monomind/cli/dist/src/orgrt/broker.d.ts +26 -0
  76. package/packages/@monomind/cli/dist/src/orgrt/broker.js +73 -0
  77. package/packages/@monomind/cli/dist/src/orgrt/bus.d.ts +5 -1
  78. package/packages/@monomind/cli/dist/src/orgrt/bus.js +5 -1
  79. package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +46 -0
  80. package/packages/@monomind/cli/dist/src/orgrt/daemon.js +201 -18
  81. package/packages/@monomind/cli/dist/src/orgrt/forwarder.d.ts +10 -5
  82. package/packages/@monomind/cli/dist/src/orgrt/forwarder.js +138 -8
  83. package/packages/@monomind/cli/dist/src/orgrt/inbox.d.ts +11 -0
  84. package/packages/@monomind/cli/dist/src/orgrt/inbox.js +56 -0
  85. package/packages/@monomind/cli/dist/src/orgrt/policy.d.ts +5 -1
  86. package/packages/@monomind/cli/dist/src/orgrt/policy.js +58 -5
  87. package/packages/@monomind/cli/dist/src/orgrt/provider.js +1 -0
  88. package/packages/@monomind/cli/dist/src/orgrt/scheduler.d.ts +13 -0
  89. package/packages/@monomind/cli/dist/src/orgrt/scheduler.js +48 -0
  90. package/packages/@monomind/cli/dist/src/orgrt/server.d.ts +9 -0
  91. package/packages/@monomind/cli/dist/src/orgrt/server.js +39 -0
  92. package/packages/@monomind/cli/dist/src/orgrt/session.d.ts +10 -1
  93. package/packages/@monomind/cli/dist/src/orgrt/session.js +24 -1
  94. package/packages/@monomind/cli/dist/src/orgrt/test-loop.d.ts +12 -0
  95. package/packages/@monomind/cli/dist/src/orgrt/test-loop.js +108 -0
  96. package/packages/@monomind/cli/dist/src/orgrt/types.d.ts +52 -52
  97. package/packages/@monomind/cli/dist/src/output.d.ts +29 -29
  98. package/packages/@monomind/cli/dist/src/output.js +22 -7
  99. package/packages/@monomind/cli/dist/src/parser.d.ts +32 -0
  100. package/packages/@monomind/cli/dist/src/parser.js +130 -5
  101. package/packages/@monomind/cli/dist/src/routing/embed-worker.js +13 -1
  102. package/packages/@monomind/cli/dist/src/routing/route-layer-factory.js +31 -0
  103. package/packages/@monomind/cli/dist/src/services/config-file-manager.d.ts +21 -0
  104. package/packages/@monomind/cli/dist/src/services/config-file-manager.js +60 -7
  105. package/packages/@monomind/cli/dist/src/services/crash-reporter.js +2 -1
  106. package/packages/@monomind/cli/dist/src/types.d.ts +2 -2
  107. package/packages/@monomind/cli/dist/src/ui/dashboard.html +243 -49
  108. package/packages/@monomind/cli/dist/src/ui/orgs-files.js +192 -0
  109. package/packages/@monomind/cli/dist/src/ui/orgs.html +79 -11
  110. package/packages/@monomind/cli/dist/src/ui/server.mjs +346 -139
  111. package/packages/@monomind/cli/dist/src/utils/input-guards.d.ts +9 -0
  112. package/packages/@monomind/cli/dist/src/utils/input-guards.js +39 -5
  113. 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(fs.readdirSync(regDir).filter(f => f.endsWith('.json')).length, MAX_AGENTS);
32
- // Preserve lastActive (peak) so statusline shows non-zero after completion.
33
- let prevLastActive = 0;
34
- try {
35
- const MAX_ACTIVITY = 64 * 1024; // 64 KiB
36
- var actStat = fs.statSync(activityPath);
37
- if (actStat.size <= MAX_ACTIVITY) {
38
- prevLastActive = (JSON.parse(fs.readFileSync(activityPath, 'utf-8'))?.swarm?.lastActive) || 0;
39
- }
40
- } catch { /* ignore */ }
41
- fs.writeFileSync(activityPath, JSON.stringify({
42
- timestamp: new Date().toISOString(),
43
- swarm: {
44
- active: active > 0,
45
- agent_count: active,
46
- coordination_active: active > 0,
47
- lastActive: Math.max(active, prevLastActive),
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
- _hooksModule.executeHooks(_hooksModule.HookEvent.AgentSpawn, {
91
- agent: {
92
- id: hookInput.agentId || hookInput.agent_id || (agentTypeBridge + '-' + Date.now()),
93
- type: agentTypeBridge,
94
- description: agentDescBridge.slice(0, 500),
95
- },
96
- }, { continueOnError: true, timeout: 1500 }).catch(function() {});
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
- // Write snapshot to FIFO queue subagent-stop pops the oldest
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
- 'snap-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6) + '.json'
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
- // Pop oldest snapshot (FIFO — matches the agent that just finished)
240
- const snapFiles = fs.readdirSync(CAPTURE_DIR)
241
- .filter(f => f.startsWith('snap-') && f.endsWith('.json'))
242
- .sort(); // lexicographic = timestamp order
243
-
244
- if (snapFiles.length === 0) {
245
- console.log('[CAPTURE:stop] No snapshot to diff — skipping');
246
- return;
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
- fs.appendFileSync(genLog, JSON.stringify({
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
- }) + '\n');
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
- fs.appendFileSync(capLog, JSON.stringify({
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
- }) + '\n');
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: (function() {
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
- if (now - lastBuild > COOLDOWN_MS) {
92
- fs.writeFileSync(lockFile, String(now), 'utf-8');
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
- _hooksModule.executeHooks(_hooksModule.HookEvent.PostEdit, {
152
- file: { path: editFileBridge, operation: 'modify' },
153
- duration: 0,
154
- }, { continueOnError: true, timeout: 1500 }).catch(function() {});
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 (require-confirmation block)
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-9]{20,}/g,
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 detected: "${match[0]}". Confirm this is intentional and document a rollback plan before proceeding.`,
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
- // Output block decision and set exit code 2 both required by Claude Code PreToolUse protocol
96
- console.log(JSON.stringify({
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
- // Output block decision and set exit code 2 both required by Claude Code PreToolUse protocol
124
- console.log(JSON.stringify({
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 (daemon optimize worker)
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.memoryUsage) {
335
- var rssBytes = perfData.memoryUsage.rss || 0;
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] Daemon RSS ' + rssMB + 'MB (>512MB threshold). Consider restarting daemon or reducing worker concurrency');
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
  }