@monoes/monomindcli 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 (328) hide show
  1. package/.claude/helpers/control-start.cjs +13 -20
  2. package/.claude/helpers/event-logger.cjs +109 -6
  3. package/.claude/helpers/graphify-freshen.cjs +19 -17
  4. package/.claude/helpers/handlers/agent-start-handler.cjs +60 -28
  5. package/.claude/helpers/handlers/capture-handler.cjs +99 -28
  6. package/.claude/helpers/handlers/edit-handler.cjs +27 -9
  7. package/.claude/helpers/handlers/gates-handler.cjs +18 -9
  8. package/.claude/helpers/handlers/loops-status-handler.cjs +1 -1
  9. package/.claude/helpers/handlers/route-handler.cjs +33 -4
  10. package/.claude/helpers/handlers/session-handler.cjs +75 -45
  11. package/.claude/helpers/handlers/session-restore-handler.cjs +0 -12
  12. package/.claude/helpers/handlers/task-handler.cjs +53 -22
  13. package/.claude/helpers/hook-handler.cjs +75 -4
  14. package/.claude/helpers/intelligence.cjs +94 -15
  15. package/.claude/helpers/session.cjs +6 -1
  16. package/.claude/helpers/statusline.cjs +10 -10
  17. package/.claude/helpers/token-tracker.cjs +3 -3
  18. package/.claude/helpers/utils/agent-registrations.cjs +41 -0
  19. package/.claude/helpers/utils/fs-helpers.cjs +183 -0
  20. package/.claude/helpers/utils/micro-agents.cjs +10 -6
  21. package/.claude/helpers/utils/monograph.cjs +73 -2
  22. package/.claude/helpers/utils/telemetry.cjs +35 -19
  23. package/README.md +74 -92
  24. package/bin/cli.js +10 -1
  25. package/dist/src/agents/registry-builder.js.map +1 -1
  26. package/dist/src/browser/dashboard/server.js.map +1 -1
  27. package/dist/src/browser/workflow/engine.d.ts.map +1 -1
  28. package/dist/src/browser/workflow/engine.js.map +1 -1
  29. package/dist/src/browser/workflow/store.d.ts +2 -2
  30. package/dist/src/browser/workflow/store.d.ts.map +1 -1
  31. package/dist/src/browser/workflow/store.js.map +1 -1
  32. package/dist/src/capabilities/manager.d.ts.map +1 -1
  33. package/dist/src/capabilities/watcher.d.ts.map +1 -1
  34. package/dist/src/commands/agent-lifecycle.js.map +1 -1
  35. package/dist/src/commands/agent-ops.d.ts.map +1 -1
  36. package/dist/src/commands/agent-ops.js +9 -15
  37. package/dist/src/commands/agent-ops.js.map +1 -1
  38. package/dist/src/commands/agent.d.ts.map +1 -1
  39. package/dist/src/commands/analyze-ast.js.map +1 -1
  40. package/dist/src/commands/analyze.d.ts.map +1 -1
  41. package/dist/src/commands/analyze.js.map +1 -1
  42. package/dist/src/commands/autopilot.d.ts.map +1 -1
  43. package/dist/src/commands/browse-action.d.ts.map +1 -1
  44. package/dist/src/commands/browse-action.js.map +1 -1
  45. package/dist/src/commands/browse-platform.d.ts.map +1 -1
  46. package/dist/src/commands/browse-workflow.d.ts.map +1 -1
  47. package/dist/src/commands/browse.d.ts +1 -1
  48. package/dist/src/commands/browse.d.ts.map +1 -1
  49. package/dist/src/commands/cleanup.d.ts.map +1 -1
  50. package/dist/src/commands/cleanup.js +20 -4
  51. package/dist/src/commands/cleanup.js.map +1 -1
  52. package/dist/src/commands/completions.d.ts.map +1 -1
  53. package/dist/src/commands/completions.js +66 -261
  54. package/dist/src/commands/completions.js.map +1 -1
  55. package/dist/src/commands/config.d.ts.map +1 -1
  56. package/dist/src/commands/crash-reporting.d.ts.map +1 -1
  57. package/dist/src/commands/design-detect.d.ts.map +1 -1
  58. package/dist/src/commands/design-palette.d.ts.map +1 -1
  59. package/dist/src/commands/doc.d.ts.map +1 -1
  60. package/dist/src/commands/doctor-env-checks.d.ts.map +1 -1
  61. package/dist/src/commands/doctor-env-checks.js +5 -0
  62. package/dist/src/commands/doctor-env-checks.js.map +1 -1
  63. package/dist/src/commands/doctor-project-checks.js.map +1 -1
  64. package/dist/src/commands/doctor.d.ts.map +1 -1
  65. package/dist/src/commands/doctor.js +7 -2
  66. package/dist/src/commands/doctor.js.map +1 -1
  67. package/dist/src/commands/guidance.d.ts.map +1 -1
  68. package/dist/src/commands/hooks-workers.d.ts.map +1 -1
  69. package/dist/src/commands/hooks-workers.js +7 -0
  70. package/dist/src/commands/hooks-workers.js.map +1 -1
  71. package/dist/src/commands/hooks.d.ts.map +1 -1
  72. package/dist/src/commands/index.d.ts.map +1 -1
  73. package/dist/src/commands/index.js +3 -0
  74. package/dist/src/commands/index.js.map +1 -1
  75. package/dist/src/commands/init.d.ts.map +1 -1
  76. package/dist/src/commands/mcp.d.ts.map +1 -1
  77. package/dist/src/commands/mcp.js +11 -6
  78. package/dist/src/commands/mcp.js.map +1 -1
  79. package/dist/src/commands/memory-admin.d.ts.map +1 -1
  80. package/dist/src/commands/memory-admin.js +12 -6
  81. package/dist/src/commands/memory-admin.js.map +1 -1
  82. package/dist/src/commands/memory-transfer.d.ts.map +1 -1
  83. package/dist/src/commands/memory-transfer.js +6 -13
  84. package/dist/src/commands/memory-transfer.js.map +1 -1
  85. package/dist/src/commands/memory.d.ts.map +1 -1
  86. package/dist/src/commands/monograph.d.ts.map +1 -1
  87. package/dist/src/commands/monograph.js +3 -3
  88. package/dist/src/commands/monograph.js.map +1 -1
  89. package/dist/src/commands/org.d.ts +3 -1
  90. package/dist/src/commands/org.d.ts.map +1 -1
  91. package/dist/src/commands/org.js +321 -127
  92. package/dist/src/commands/org.js.map +1 -1
  93. package/dist/src/commands/performance.d.ts.map +1 -1
  94. package/dist/src/commands/platforms.d.ts +1 -1
  95. package/dist/src/commands/platforms.d.ts.map +1 -1
  96. package/dist/src/commands/providers.d.ts.map +1 -1
  97. package/dist/src/commands/report-crash.d.ts.map +1 -1
  98. package/dist/src/commands/route.d.ts.map +1 -1
  99. package/dist/src/commands/search-universal.d.ts.map +1 -1
  100. package/dist/src/commands/security.d.ts.map +1 -1
  101. package/dist/src/commands/session.d.ts.map +1 -1
  102. package/dist/src/commands/start.d.ts.map +1 -1
  103. package/dist/src/commands/start.js +142 -37
  104. package/dist/src/commands/start.js.map +1 -1
  105. package/dist/src/commands/status.d.ts.map +1 -1
  106. package/dist/src/commands/swarm.d.ts.map +1 -1
  107. package/dist/src/commands/task.d.ts.map +1 -1
  108. package/dist/src/commands/tokens.d.ts.map +1 -1
  109. package/dist/src/commands/tokens.js +21 -3
  110. package/dist/src/commands/tokens.js.map +1 -1
  111. package/dist/src/commands/transfer-store.d.ts.map +1 -1
  112. package/dist/src/commands/update.d.ts.map +1 -1
  113. package/dist/src/config-adapter.d.ts.map +1 -1
  114. package/dist/src/config-adapter.js +37 -17
  115. package/dist/src/config-adapter.js.map +1 -1
  116. package/dist/src/consensus/audit-writer.d.ts.map +1 -1
  117. package/dist/src/index.d.ts.map +1 -1
  118. package/dist/src/index.js +64 -28
  119. package/dist/src/index.js.map +1 -1
  120. package/dist/src/init/claudemd-generator.d.ts.map +1 -1
  121. package/dist/src/init/claudemd-generator.js +44 -11
  122. package/dist/src/init/claudemd-generator.js.map +1 -1
  123. package/dist/src/init/executor.d.ts.map +1 -1
  124. package/dist/src/init/executor.js +93 -55
  125. package/dist/src/init/executor.js.map +1 -1
  126. package/dist/src/init/mcp-generator.js.map +1 -1
  127. package/dist/src/mcp-client.d.ts +8 -3
  128. package/dist/src/mcp-client.d.ts.map +1 -1
  129. package/dist/src/mcp-client.js +37 -3
  130. package/dist/src/mcp-client.js.map +1 -1
  131. package/dist/src/mcp-server.d.ts.map +1 -1
  132. package/dist/src/mcp-server.js +8 -2
  133. package/dist/src/mcp-server.js.map +1 -1
  134. package/dist/src/mcp-tools/agent-tools.d.ts +20 -0
  135. package/dist/src/mcp-tools/agent-tools.d.ts.map +1 -1
  136. package/dist/src/mcp-tools/agent-tools.js +10 -5
  137. package/dist/src/mcp-tools/agent-tools.js.map +1 -1
  138. package/dist/src/mcp-tools/analyze-tools.d.ts.map +1 -1
  139. package/dist/src/mcp-tools/auto-install.d.ts +8 -8
  140. package/dist/src/mcp-tools/auto-install.d.ts.map +1 -1
  141. package/dist/src/mcp-tools/auto-install.js.map +1 -1
  142. package/dist/src/mcp-tools/browser-tools.d.ts.map +1 -1
  143. package/dist/src/mcp-tools/browser-tools.js.map +1 -1
  144. package/dist/src/mcp-tools/claims-tools.d.ts.map +1 -1
  145. package/dist/src/mcp-tools/coherence/causal-infer.d.ts.map +1 -1
  146. package/dist/src/mcp-tools/coherence/causal-infer.js.map +1 -1
  147. package/dist/src/mcp-tools/coherence/coherence-check.d.ts.map +1 -1
  148. package/dist/src/mcp-tools/coherence/consensus-verify.d.ts.map +1 -1
  149. package/dist/src/mcp-tools/coherence/memory-gate.d.ts.map +1 -1
  150. package/dist/src/mcp-tools/coherence/quantum-topology.d.ts.map +1 -1
  151. package/dist/src/mcp-tools/coherence/spectral-analyze.d.ts.map +1 -1
  152. package/dist/src/mcp-tools/coherence/spectral-analyze.js.map +1 -1
  153. package/dist/src/mcp-tools/coherence/types.d.ts +31 -31
  154. package/dist/src/mcp-tools/embeddings-tools.js.map +1 -1
  155. package/dist/src/mcp-tools/github-tools.d.ts.map +1 -1
  156. package/dist/src/mcp-tools/github-tools.js +30 -8
  157. package/dist/src/mcp-tools/github-tools.js.map +1 -1
  158. package/dist/src/mcp-tools/graphify-tools.d.ts.map +1 -1
  159. package/dist/src/mcp-tools/graphify-tools.js +11 -2
  160. package/dist/src/mcp-tools/graphify-tools.js.map +1 -1
  161. package/dist/src/mcp-tools/guidance-tools.d.ts.map +1 -1
  162. package/dist/src/mcp-tools/guidance-tools.js.map +1 -1
  163. package/dist/src/mcp-tools/hive-mind-tools.d.ts.map +1 -1
  164. package/dist/src/mcp-tools/hive-mind-tools.js +56 -35
  165. package/dist/src/mcp-tools/hive-mind-tools.js.map +1 -1
  166. package/dist/src/mcp-tools/hooks-embedding.js.map +1 -1
  167. package/dist/src/mcp-tools/hooks-tools.d.ts.map +1 -1
  168. package/dist/src/mcp-tools/knowledge-tools.d.ts.map +1 -1
  169. package/dist/src/mcp-tools/knowledge-tools.js +9 -1
  170. package/dist/src/mcp-tools/knowledge-tools.js.map +1 -1
  171. package/dist/src/mcp-tools/monograph-compat.d.ts.map +1 -1
  172. package/dist/src/mcp-tools/monograph-compat.js +56 -14
  173. package/dist/src/mcp-tools/monograph-compat.js.map +1 -1
  174. package/dist/src/mcp-tools/monograph-tools.d.ts.map +1 -1
  175. package/dist/src/mcp-tools/monograph-tools.js +88 -41
  176. package/dist/src/mcp-tools/monograph-tools.js.map +1 -1
  177. package/dist/src/mcp-tools/neural-tools.js.map +1 -1
  178. package/dist/src/mcp-tools/quality/coverage-analysis/prioritize-gaps.d.ts +36 -36
  179. package/dist/src/mcp-tools/quality/coverage-analysis/prioritize-gaps.d.ts.map +1 -1
  180. package/dist/src/mcp-tools/quality/security-compliance/detect-secrets.d.ts +4 -4
  181. package/dist/src/mcp-tools/quality/security-compliance/detect-secrets.d.ts.map +1 -1
  182. package/dist/src/mcp-tools/security-tools.d.ts.map +1 -1
  183. package/dist/src/mcp-tools/security-tools.js.map +1 -1
  184. package/dist/src/mcp-tools/swarm-tools.d.ts.map +1 -1
  185. package/dist/src/mcp-tools/swarm-tools.js +6 -4
  186. package/dist/src/mcp-tools/swarm-tools.js.map +1 -1
  187. package/dist/src/mcp-tools/system-tools.d.ts.map +1 -1
  188. package/dist/src/mcp-tools/system-tools.js +4 -4
  189. package/dist/src/mcp-tools/system-tools.js.map +1 -1
  190. package/dist/src/mcp-tools/transfer-tools.d.ts.map +1 -1
  191. package/dist/src/mcp-tools/types.d.ts +20 -0
  192. package/dist/src/mcp-tools/types.d.ts.map +1 -1
  193. package/dist/src/mcp-tools/types.js +36 -1
  194. package/dist/src/mcp-tools/types.js.map +1 -1
  195. package/dist/src/memory/ewc-consolidation.d.ts +12 -0
  196. package/dist/src/memory/ewc-consolidation.d.ts.map +1 -1
  197. package/dist/src/memory/ewc-consolidation.js.map +1 -1
  198. package/dist/src/memory/hnsw-operations.d.ts +13 -1
  199. package/dist/src/memory/hnsw-operations.d.ts.map +1 -1
  200. package/dist/src/memory/hnsw-operations.js +66 -14
  201. package/dist/src/memory/hnsw-operations.js.map +1 -1
  202. package/dist/src/memory/intelligence.d.ts.map +1 -1
  203. package/dist/src/memory/intelligence.js +99 -3
  204. package/dist/src/memory/intelligence.js.map +1 -1
  205. package/dist/src/memory/memory-bridge.d.ts +7 -0
  206. package/dist/src/memory/memory-bridge.d.ts.map +1 -1
  207. package/dist/src/memory/memory-bridge.js +20 -0
  208. package/dist/src/memory/memory-bridge.js.map +1 -1
  209. package/dist/src/memory/memory-initializer.d.ts.map +1 -1
  210. package/dist/src/memory/sona-optimizer.d.ts +12 -0
  211. package/dist/src/memory/sona-optimizer.d.ts.map +1 -1
  212. package/dist/src/memory/sona-optimizer.js.map +1 -1
  213. package/dist/src/monovector/coverage-router.js.map +1 -1
  214. package/dist/src/monovector/coverage-tools.d.ts.map +1 -1
  215. package/dist/src/monovector/diff-classifier.d.ts.map +1 -1
  216. package/dist/src/monovector/diff-classifier.js.map +1 -1
  217. package/dist/src/monovector/init-state.js.map +1 -1
  218. package/dist/src/observability/replay-reader.d.ts.map +1 -1
  219. package/dist/src/orgrt/broker.d.ts +26 -0
  220. package/dist/src/orgrt/broker.d.ts.map +1 -0
  221. package/dist/src/orgrt/broker.js +73 -0
  222. package/dist/src/orgrt/broker.js.map +1 -0
  223. package/dist/src/orgrt/bus.d.ts +5 -1
  224. package/dist/src/orgrt/bus.d.ts.map +1 -1
  225. package/dist/src/orgrt/bus.js +5 -1
  226. package/dist/src/orgrt/bus.js.map +1 -1
  227. package/dist/src/orgrt/daemon.d.ts +46 -0
  228. package/dist/src/orgrt/daemon.d.ts.map +1 -1
  229. package/dist/src/orgrt/daemon.js +201 -18
  230. package/dist/src/orgrt/daemon.js.map +1 -1
  231. package/dist/src/orgrt/forwarder.d.ts +10 -5
  232. package/dist/src/orgrt/forwarder.d.ts.map +1 -1
  233. package/dist/src/orgrt/forwarder.js +138 -8
  234. package/dist/src/orgrt/forwarder.js.map +1 -1
  235. package/dist/src/orgrt/inbox.d.ts +11 -0
  236. package/dist/src/orgrt/inbox.d.ts.map +1 -0
  237. package/dist/src/orgrt/inbox.js +56 -0
  238. package/dist/src/orgrt/inbox.js.map +1 -0
  239. package/dist/src/orgrt/mailbox.d.ts.map +1 -1
  240. package/dist/src/orgrt/policy.d.ts +5 -1
  241. package/dist/src/orgrt/policy.d.ts.map +1 -1
  242. package/dist/src/orgrt/policy.js +58 -5
  243. package/dist/src/orgrt/policy.js.map +1 -1
  244. package/dist/src/orgrt/provider.d.ts.map +1 -1
  245. package/dist/src/orgrt/provider.js +1 -0
  246. package/dist/src/orgrt/provider.js.map +1 -1
  247. package/dist/src/orgrt/scheduler.d.ts +13 -0
  248. package/dist/src/orgrt/scheduler.d.ts.map +1 -0
  249. package/dist/src/orgrt/scheduler.js +48 -0
  250. package/dist/src/orgrt/scheduler.js.map +1 -0
  251. package/dist/src/orgrt/server.d.ts +9 -0
  252. package/dist/src/orgrt/server.d.ts.map +1 -0
  253. package/dist/src/orgrt/server.js +39 -0
  254. package/dist/src/orgrt/server.js.map +1 -0
  255. package/dist/src/orgrt/session.d.ts +10 -1
  256. package/dist/src/orgrt/session.d.ts.map +1 -1
  257. package/dist/src/orgrt/session.js +24 -1
  258. package/dist/src/orgrt/session.js.map +1 -1
  259. package/dist/src/orgrt/test-loop.d.ts +12 -0
  260. package/dist/src/orgrt/test-loop.d.ts.map +1 -0
  261. package/dist/src/orgrt/test-loop.js +108 -0
  262. package/dist/src/orgrt/test-loop.js.map +1 -0
  263. package/dist/src/orgrt/types.d.ts +52 -52
  264. package/dist/src/output.d.ts +29 -29
  265. package/dist/src/output.d.ts.map +1 -1
  266. package/dist/src/output.js +22 -7
  267. package/dist/src/output.js.map +1 -1
  268. package/dist/src/parser.d.ts +32 -0
  269. package/dist/src/parser.d.ts.map +1 -1
  270. package/dist/src/parser.js +130 -5
  271. package/dist/src/parser.js.map +1 -1
  272. package/dist/src/pricing/model-pricing.d.ts.map +1 -1
  273. package/dist/src/production/circuit-breaker.d.ts.map +1 -1
  274. package/dist/src/production/circuit-breaker.js.map +1 -1
  275. package/dist/src/production/error-handler.d.ts.map +1 -1
  276. package/dist/src/production/error-handler.js.map +1 -1
  277. package/dist/src/production/monitoring.d.ts.map +1 -1
  278. package/dist/src/production/monitoring.js.map +1 -1
  279. package/dist/src/production/rate-limiter.d.ts.map +1 -1
  280. package/dist/src/production/rate-limiter.js.map +1 -1
  281. package/dist/src/production/retry.d.ts.map +1 -1
  282. package/dist/src/production/retry.js.map +1 -1
  283. package/dist/src/prompt.d.ts.map +1 -1
  284. package/dist/src/prompt.js.map +1 -1
  285. package/dist/src/routing/embed-worker.js +13 -1
  286. package/dist/src/routing/embed-worker.js.map +1 -1
  287. package/dist/src/routing/llm-caller.js.map +1 -1
  288. package/dist/src/routing/route-layer-factory.d.ts.map +1 -1
  289. package/dist/src/routing/route-layer-factory.js +31 -0
  290. package/dist/src/routing/route-layer-factory.js.map +1 -1
  291. package/dist/src/services/config-file-manager.d.ts +21 -0
  292. package/dist/src/services/config-file-manager.d.ts.map +1 -1
  293. package/dist/src/services/config-file-manager.js +60 -7
  294. package/dist/src/services/config-file-manager.js.map +1 -1
  295. package/dist/src/services/crash-reporter.d.ts.map +1 -1
  296. package/dist/src/services/crash-reporter.js +2 -1
  297. package/dist/src/services/crash-reporter.js.map +1 -1
  298. package/dist/src/suggest.d.ts.map +1 -1
  299. package/dist/src/suggest.js.map +1 -1
  300. package/dist/src/transfer/export.js.map +1 -1
  301. package/dist/src/transfer/ipfs/client.js.map +1 -1
  302. package/dist/src/transfer/ipfs/upload.js.map +1 -1
  303. package/dist/src/transfer/models/seraphine.d.ts.map +1 -1
  304. package/dist/src/transfer/storage/gcs.js.map +1 -1
  305. package/dist/src/transfer/store/discovery.d.ts.map +1 -1
  306. package/dist/src/transfer/store/discovery.js.map +1 -1
  307. package/dist/src/transfer/store/download.d.ts.map +1 -1
  308. package/dist/src/transfer/store/download.js.map +1 -1
  309. package/dist/src/transfer/store/index.d.ts.map +1 -1
  310. package/dist/src/transfer/store/index.js.map +1 -1
  311. package/dist/src/transfer/store/publish.d.ts.map +1 -1
  312. package/dist/src/transfer/store/publish.js.map +1 -1
  313. package/dist/src/transfer/store/search.js.map +1 -1
  314. package/dist/src/types.d.ts +2 -2
  315. package/dist/src/types.d.ts.map +1 -1
  316. package/dist/src/types.js.map +1 -1
  317. package/dist/src/ui/dashboard.html +243 -49
  318. package/dist/src/ui/orgs-files.js +192 -0
  319. package/dist/src/ui/orgs.html +79 -11
  320. package/dist/src/ui/server.mjs +346 -139
  321. package/dist/src/update/checker.js.map +1 -1
  322. package/dist/src/update/rate-limiter.js.map +1 -1
  323. package/dist/src/utils/input-guards.d.ts +9 -0
  324. package/dist/src/utils/input-guards.d.ts.map +1 -1
  325. package/dist/src/utils/input-guards.js +39 -5
  326. package/dist/src/utils/input-guards.js.map +1 -1
  327. package/dist/tsconfig.tsbuildinfo +1 -1
  328. package/package.json +15 -15
@@ -13,6 +13,7 @@
13
13
 
14
14
  const path = require('path');
15
15
  const fs = require('fs');
16
+ const { claimLock, releaseLock } = require('./utils/fs-helpers.cjs');
16
17
 
17
18
  // Resolve base dir at require-time so tests can inject CLAUDE_PROJECT_DIR
18
19
  // per fresh require() call.
@@ -35,6 +36,76 @@ function ensureDataDir() {
35
36
  try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch (_) {}
36
37
  }
37
38
 
39
+ // ── multi-process-safe store flush (P1-14) ──────────────────────────────────
40
+ // STORE_FILE (auto-memory-store.json) is read once into `_entries` at init()
41
+ // and can be written by more than one code path — consolidate() and
42
+ // bootstrapFromDb() — each of which may be running in a *different* process
43
+ // (e.g. the long-lived MCP server process and a short-lived CJS hook
44
+ // subprocess launched concurrently). Without a lock + re-read-before-write,
45
+ // whichever process flushes last silently erases whatever the other process
46
+ // added since its own load. mergeAndWriteStore() re-reads the current
47
+ // on-disk file immediately before writing, merges the caller's updates in by
48
+ // `id` (keeping whichever record has the newer `ts`), and writes atomically
49
+ // via tmp+rename, guarded by a short advisory lock.
50
+
51
+ const STORE_LOCK_FILE = STORE_FILE + '.lock';
52
+ const STORE_LOCK_STALE_MS = 10 * 1000; // single JSON write — 10s is generous
53
+
54
+ // Uses fs-helpers.cjs's claimLock/releaseLock: unlike an unlink-then-recreate
55
+ // stale-break, it reclaims via an atomic rename, so two processes that both
56
+ // decide the lock is stale can't both end up believing they hold it (a real
57
+ // TOCTOU race a previous unlink-then-create version of this had — best-effort
58
+ // either way, since a missed lock still merges via mergeAndWriteStore's
59
+ // re-read, this just narrows the window further).
60
+ function claimStoreLock() {
61
+ return claimLock(STORE_LOCK_FILE, STORE_LOCK_STALE_MS);
62
+ }
63
+
64
+ function releaseStoreLock() {
65
+ releaseLock(STORE_LOCK_FILE);
66
+ }
67
+
68
+ /**
69
+ * Merge `updates` (new or changed entries) into the on-disk store and write
70
+ * the result atomically. Re-reads STORE_FILE immediately before writing so a
71
+ * concurrent flush from another process is merged, not clobbered. When both
72
+ * sides have an entry with the same `id`, the one with the newer `ts` wins.
73
+ */
74
+ function mergeAndWriteStore(updates, capTo) {
75
+ var acquired = claimStoreLock();
76
+ if (!acquired) acquired = claimStoreLock(); // one retry — lock is short-lived
77
+
78
+ try {
79
+ var diskRaw = safeReadJson(STORE_FILE);
80
+ var diskEntries = Array.isArray(diskRaw) ? diskRaw : [];
81
+ var merged = new Map();
82
+ for (var i = 0; i < diskEntries.length; i++) {
83
+ var d = diskEntries[i];
84
+ if (d && d.id) merged.set(String(d.id), d);
85
+ }
86
+ for (var j = 0; j < (updates || []).length; j++) {
87
+ var u = updates[j];
88
+ if (!u || !u.id) continue;
89
+ var key = String(u.id);
90
+ var existing = merged.get(key);
91
+ if (!existing || (u.ts || 0) >= (existing.ts || 0)) {
92
+ merged.set(key, u);
93
+ }
94
+ }
95
+ var result = Array.from(merged.values());
96
+ if (capTo && result.length > capTo) {
97
+ result.sort(function(a, b) { return (b.ts || 0) - (a.ts || 0); });
98
+ result = result.slice(0, capTo);
99
+ }
100
+ var tmpPath = STORE_FILE + '.' + process.pid + '.tmp';
101
+ fs.writeFileSync(tmpPath, JSON.stringify(result, null, 2), 'utf-8');
102
+ fs.renameSync(tmpPath, STORE_FILE);
103
+ return result;
104
+ } finally {
105
+ if (acquired) releaseStoreLock();
106
+ }
107
+ }
108
+
38
109
  function safeReadJson(filePath) {
39
110
  try {
40
111
  if (!fs.existsSync(filePath)) return null;
@@ -274,7 +345,11 @@ function consolidate() {
274
345
  }
275
346
 
276
347
  if (newStoreEntries.length > 0) {
277
- // Merge with existing store entries
348
+ // Decide which of newStoreEntries are worth adding, based on a read of
349
+ // the store as of "now". The actual write below re-reads immediately
350
+ // before flushing (mergeAndWriteStore), so a concurrent process's writes
351
+ // in between are merged rather than clobbered — this initial read is
352
+ // only used for the near-duplicate decision, not as the write's base.
278
353
  var existing = safeReadJson(STORE_FILE);
279
354
  var store = Array.isArray(existing) ? existing : [];
280
355
  var existingIds = new Set(store.map(function(e) { return e && e.id; }));
@@ -283,25 +358,23 @@ function consolidate() {
283
358
  var fileSetKey = function(e) { return (e.files || []).slice().sort().join('|'); };
284
359
  var seenSummaries = new Set(store.map(function(e) { return e && e.summary; }));
285
360
  var seenFileSets = new Set(store.filter(function(e) { return e && Array.isArray(e.files) && e.files.length > 0; }).map(fileSetKey));
361
+ var toAdd = [];
286
362
  var addedCount = 0;
287
363
  for (var j = 0; j < newStoreEntries.length; j++) {
288
364
  var ne = newStoreEntries[j];
289
365
  if (existingIds.has(ne.id)) continue;
290
366
  if (seenSummaries.has(ne.summary)) continue;
291
367
  if (ne.files && ne.files.length > 0 && seenFileSets.has(fileSetKey(ne))) continue;
292
- store.push(ne);
368
+ toAdd.push(ne);
293
369
  seenSummaries.add(ne.summary);
294
370
  if (ne.files && ne.files.length > 0) seenFileSets.add(fileSetKey(ne));
295
371
  addedCount++;
296
372
  }
297
- // Cap store at 200 entries to prevent unbounded growth
298
- if (store.length > 200) {
299
- store.sort(function(a, b) { return (b.ts || 0) - (a.ts || 0); });
300
- store = store.slice(0, 200);
373
+ if (toAdd.length > 0) {
374
+ try {
375
+ mergeAndWriteStore(toAdd, 200);
376
+ } catch (_) {}
301
377
  }
302
- try {
303
- fs.writeFileSync(STORE_FILE, JSON.stringify(store, null, 2), 'utf-8');
304
- } catch (_) {}
305
378
  count += addedCount;
306
379
  }
307
380
 
@@ -395,19 +468,20 @@ function bootstrapFromDb(db) {
395
468
  if (!db || _entries.length >= 5) return 0;
396
469
  try {
397
470
  var hubs = db.prepare(
398
- "SELECT n.name, n.label, n.file, COUNT(e.id) AS deg " +
399
- "FROM nodes n JOIN edges e ON (e.source = n.id OR e.target = n.id) " +
400
- "WHERE n.label IN ('File','Function','Class') AND n.file NOT LIKE '%node_modules%' AND n.file NOT LIKE '%dist/%' " +
471
+ "SELECT n.name, n.label, n.file_path AS file, COUNT(e.id) AS deg " +
472
+ "FROM nodes n JOIN edges e ON (e.source_id = n.id OR e.target_id = n.id) " +
473
+ "WHERE n.label IN ('File','Function','Class') AND n.file_path NOT LIKE '%node_modules%' AND n.file_path NOT LIKE '%dist/%' " +
401
474
  "GROUP BY n.id ORDER BY deg DESC LIMIT 10"
402
475
  ).all();
403
476
  if (hubs.length === 0) return 0;
404
477
  var existingIds = new Set(_entries.map(function(e) { return e.id; }));
478
+ var newHubEntries = [];
405
479
  var added = 0;
406
480
  for (var hi = 0; hi < hubs.length; hi++) {
407
481
  var h = hubs[hi];
408
482
  var bId = 'bootstrap-hub-' + hi;
409
483
  if (existingIds.has(bId)) continue;
410
- _entries.push({
484
+ var hubEntry = {
411
485
  id: bId,
412
486
  type: 'hub',
413
487
  content: h.name + ' (' + h.label + ') — ' + (h.file || '').replace(CWD + '/', '') + ' (' + h.deg + ' connections)',
@@ -415,12 +489,17 @@ function bootstrapFromDb(db) {
415
489
  confidence: 0.4,
416
490
  files: h.file ? [h.file] : [],
417
491
  ts: Date.now(),
418
- });
492
+ };
493
+ _entries.push(hubEntry);
494
+ newHubEntries.push(hubEntry);
419
495
  added++;
420
496
  }
421
497
  if (added > 0) {
422
498
  ensureDataDir();
423
- try { fs.writeFileSync(STORE_FILE, JSON.stringify(_entries, null, 2), 'utf-8'); } catch (_) {}
499
+ // mergeAndWriteStore re-reads STORE_FILE immediately before writing, so
500
+ // this only ever adds these hub entries into whatever is currently on
501
+ // disk — it never overwrites entries a concurrent process added.
502
+ try { mergeAndWriteStore(newHubEntries, 200); } catch (_) {}
424
503
  }
425
504
  return added;
426
505
  } catch (_) { return 0; }
@@ -9,6 +9,7 @@
9
9
 
10
10
  const path = require('path');
11
11
  const fs = require('fs');
12
+ const { atomicWriteFileSync } = require('./utils/fs-helpers.cjs');
12
13
 
13
14
  const CWD = process.env.CLAUDE_PROJECT_DIR || process.cwd();
14
15
 
@@ -54,7 +55,11 @@ function readCurrent() {
54
55
  function writeCurrent(data) {
55
56
  ensureDir();
56
57
  try {
57
- fs.writeFileSync(CURRENT_FILE, JSON.stringify(data, null, 2), 'utf-8');
58
+ // P2-21: tmp+rename — current.json is read+written by metric()/update()
59
+ // from multiple concurrent hook processes (pre-task, post-edit, etc. can
60
+ // all fire in one batched tool-call message); a plain writeFileSync lets
61
+ // a concurrent reader observe a partially-written file.
62
+ atomicWriteFileSync(CURRENT_FILE, JSON.stringify(data, null, 2), 'utf-8');
58
63
  } catch (_) {}
59
64
  }
60
65
 
@@ -18,6 +18,7 @@ const fs = require('fs');
18
18
  const path = require('path');
19
19
  const { execSync, spawnSync } = require('child_process');
20
20
  const os = require('os');
21
+ const { cleanEntries } = require('./utils/fs-helpers.cjs');
21
22
 
22
23
  // Configuration
23
24
  const CONFIG = {
@@ -197,8 +198,7 @@ function getModelFromSessionJSONL() {
197
198
  if (!fs.existsSync(projectsDir)) return null;
198
199
 
199
200
  // Most recently modified JSONL = current (or latest) session
200
- const files = fs.readdirSync(projectsDir)
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 = fs.readdirSync(sessDir).filter(f => f.endsWith('.json')).length;
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 = fs.readdirSync(scanDir).filter(f => f.endsWith('.json')).length;
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 = fs.readdirSync(regDir).filter(f => f.endsWith('.json'));
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 = fs.readdirSync(adrPath).filter(f =>
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 = fs.readdirSync(hooksDir).filter(f => f.endsWith('.js') || f.endsWith('.sh')).length;
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 = fs.readdirSync(loopsDir).filter(f =>
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 = fs.readdirSync(loopsDir).filter(f => f.endsWith('-hil.md'));
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 = fs.readdirSync(runsDir).filter(f => f.endsWith('.jsonl'));
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 = fs.readdirSync(dir);
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
- fs.mkdirSync(path.join(CWD, '.monomind'), { recursive: true });
139
- fs.writeFileSync(indexPath, JSON.stringify({ patterns: patterns, builtAt: new Date().toISOString(), totalAgentsScanned: patterns.length }));
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
- fs.mkdirSync(knowledgeDir, { recursive: true });
338
- fs.writeFileSync(chunksFile, newLines.length > 0 ? newLines.join('\n') + '\n' : '', 'utf-8');
339
- fs.writeFileSync(hashFile, contentHash, 'utf-8');
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
  }