@reticlehq/server 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (472) hide show
  1. package/SKILL.md +719 -0
  2. package/dist/bridge/auth-failure-reason.d.ts +19 -0
  3. package/dist/bridge/auth-failure-reason.js +29 -0
  4. package/dist/{bridge-security.d.ts → bridge/bridge-security.d.ts} +1 -1
  5. package/dist/{bridge-security.js → bridge/bridge-security.js} +1 -1
  6. package/dist/{bridge.d.ts → bridge/bridge.d.ts} +13 -1
  7. package/dist/{bridge.js → bridge/bridge.js} +95 -15
  8. package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +9 -11
  9. package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +18 -10
  10. package/dist/{pairing-token.js → bridge/pairing-token.js} +1 -1
  11. package/dist/capsule/capsule-store.d.ts +8 -3
  12. package/dist/capsule/capsule-store.js +7 -2
  13. package/dist/capsule/capsule.js +3 -3
  14. package/dist/capsule/causal-summary.d.ts +8 -0
  15. package/dist/capsule/causal-summary.js +22 -12
  16. package/dist/capsule/divergence.d.ts +9 -4
  17. package/dist/capsule/divergence.js +8 -8
  18. package/dist/capsule/predicate-to-links.js +11 -8
  19. package/dist/cli/cli-doctor.d.ts +1 -0
  20. package/dist/cli/cli-doctor.js +135 -0
  21. package/dist/{cli-flow-commands.d.ts → cli/cli-flow-commands.d.ts} +2 -2
  22. package/dist/{cli-flow-commands.js → cli/cli-flow-commands.js} +15 -15
  23. package/dist/{cli-launch.d.ts → cli/cli-launch.d.ts} +32 -3
  24. package/dist/cli/cli-launch.js +171 -0
  25. package/dist/cli/cli-parse-feedback.d.ts +31 -0
  26. package/dist/cli/cli-parse-feedback.js +56 -0
  27. package/dist/{cli-parse.d.ts → cli/cli-parse.d.ts} +6 -19
  28. package/dist/{cli-parse.js → cli/cli-parse.js} +118 -81
  29. package/dist/{cli-port.d.ts → cli/cli-port.d.ts} +12 -0
  30. package/dist/{cli-port.js → cli/cli-port.js} +9 -9
  31. package/dist/cli/cli-update-commands.d.ts +4 -0
  32. package/dist/cli/cli-update-commands.js +93 -0
  33. package/dist/{cli-verify.js → cli/cli-verify.js} +14 -14
  34. package/dist/{cloud-cli.js → cli/cloud-cli.js} +29 -24
  35. package/dist/cli/doctor-daemon-line.d.ts +37 -0
  36. package/dist/cli/doctor-daemon-line.js +35 -0
  37. package/dist/cli/port-holder.d.ts +43 -0
  38. package/dist/cli/port-holder.js +99 -0
  39. package/dist/cli.d.ts +2 -2
  40. package/dist/cli.js +302 -100
  41. package/dist/cloud/cloud-config.d.ts +0 -1
  42. package/dist/cloud/cloud-config.js +9 -9
  43. package/dist/cloud/cloud-sync.d.ts +1 -7
  44. package/dist/cloud/cloud-sync.js +12 -12
  45. package/dist/crawl/crawl-empty.d.ts +16 -0
  46. package/dist/crawl/crawl-empty.js +24 -0
  47. package/dist/crawl/crawl-tools.js +4 -1
  48. package/dist/crawl/crawl.js +26 -7
  49. package/dist/daemon/daemon-resilience.d.ts +73 -0
  50. package/dist/daemon/daemon-resilience.js +279 -0
  51. package/dist/daemon/daemon-usefulness.d.ts +61 -0
  52. package/dist/daemon/daemon-usefulness.js +62 -0
  53. package/dist/{daemon.d.ts → daemon/daemon.d.ts} +33 -8
  54. package/dist/{daemon.js → daemon/daemon.js} +105 -48
  55. package/dist/daemon/heartbeat.d.ts +115 -0
  56. package/dist/daemon/heartbeat.js +141 -0
  57. package/dist/daemon/idle-grace.d.ts +32 -0
  58. package/dist/daemon/idle-grace.js +41 -0
  59. package/dist/{idle-shutdown.d.ts → daemon/idle-shutdown.d.ts} +23 -2
  60. package/dist/{idle-shutdown.js → daemon/idle-shutdown.js} +37 -6
  61. package/dist/daemon/port-presence.d.ts +76 -0
  62. package/dist/daemon/port-presence.js +93 -0
  63. package/dist/daemon/state-dir.d.ts +7 -0
  64. package/dist/daemon/state-dir.js +59 -0
  65. package/dist/daemon/unclean-predecessor.d.ts +23 -0
  66. package/dist/daemon/unclean-predecessor.js +27 -0
  67. package/dist/domain/domain-model.js +2 -2
  68. package/dist/events/body-failures.js +8 -8
  69. package/dist/events/contradictions.d.ts +1 -1
  70. package/dist/events/contradictions.js +60 -10
  71. package/dist/events/echo-mismatch.js +10 -10
  72. package/dist/events/event-filters.js +6 -6
  73. package/dist/events/json-salvage.d.ts +2 -1
  74. package/dist/events/json-salvage.js +6 -6
  75. package/dist/events/observed-in-window.d.ts +12 -0
  76. package/dist/events/observed-in-window.js +21 -0
  77. package/dist/events/predicate-eval.d.ts +53 -20
  78. package/dist/events/predicate-eval.js +179 -55
  79. package/dist/events/predicate-parse.d.ts +23 -0
  80. package/dist/events/predicate-parse.js +54 -0
  81. package/dist/events/predicate.d.ts +7 -2
  82. package/dist/events/predicate.js +126 -35
  83. package/dist/events/reconcile.js +5 -5
  84. package/dist/events/ring-buffer.js +2 -17
  85. package/dist/events/stale-response.js +6 -6
  86. package/dist/events/testid-near-miss.d.ts +22 -0
  87. package/dist/events/testid-near-miss.js +33 -0
  88. package/dist/events/unit-mismatch.js +8 -8
  89. package/dist/flows/affected-tools.js +19 -4
  90. package/dist/flows/affected.js +1 -1
  91. package/dist/flows/annotate-target.d.ts +1 -0
  92. package/dist/flows/annotate-target.js +20 -0
  93. package/dist/flows/annotate-tools.js +25 -3
  94. package/dist/flows/annotate.js +17 -4
  95. package/dist/flows/assertion-integrity.d.ts +2 -1
  96. package/dist/flows/assertion-tiers-store.d.ts +3 -66
  97. package/dist/flows/assertion-tiers-store.js +2 -2
  98. package/dist/flows/attributed-failure.d.ts +17 -0
  99. package/dist/flows/attributed-failure.js +20 -0
  100. package/dist/flows/buddy-status.d.ts +2 -1
  101. package/dist/flows/buddy-status.js +2 -2
  102. package/dist/flows/coverage.d.ts +3 -2
  103. package/dist/flows/coverage.js +2 -2
  104. package/dist/flows/decision.js +24 -9
  105. package/dist/flows/empty-flow.d.ts +6 -0
  106. package/dist/flows/empty-flow.js +24 -0
  107. package/dist/flows/flake-store.d.ts +0 -1
  108. package/dist/flows/flake-store.js +1 -1
  109. package/dist/flows/flake.d.ts +2 -1
  110. package/dist/flows/flake.js +2 -2
  111. package/dist/flows/flow-classify.js +12 -0
  112. package/dist/flows/flow-replay-run.js +4 -4
  113. package/dist/flows/flow-replay.d.ts +6 -8
  114. package/dist/flows/flow-replay.js +132 -48
  115. package/dist/flows/flow-report.js +1 -1
  116. package/dist/flows/flow-scope.d.ts +1 -6
  117. package/dist/flows/flow-step-runners.d.ts +26 -6
  118. package/dist/flows/flow-step-runners.js +156 -6
  119. package/dist/flows/flow-success.js +20 -15
  120. package/dist/flows/flow-tools.js +63 -21
  121. package/dist/flows/flows.js +15 -11
  122. package/dist/flows/gate.d.ts +4 -3
  123. package/dist/flows/gate.js +1 -1
  124. package/dist/flows/heal-run.d.ts +0 -8
  125. package/dist/flows/heal-run.js +5 -5
  126. package/dist/flows/heal.js +3 -3
  127. package/dist/flows/predicate-to-expect.d.ts +38 -0
  128. package/dist/flows/predicate-to-expect.js +95 -0
  129. package/dist/flows/replay.d.ts +16 -1
  130. package/dist/flows/replay.js +58 -22
  131. package/dist/flows/role-anchor-nearest.d.ts +6 -0
  132. package/dist/flows/role-anchor-nearest.js +45 -0
  133. package/dist/flows/role-drift-reason.d.ts +20 -0
  134. package/dist/flows/role-drift-reason.js +39 -0
  135. package/dist/flows/server-verify.d.ts +1 -1
  136. package/dist/flows/server-verify.js +5 -5
  137. package/dist/flows/suite-flakes.d.ts +33 -0
  138. package/dist/flows/suite-flakes.js +32 -0
  139. package/dist/flows/verify-change-tools.js +32 -10
  140. package/dist/flows/verify-change-verdict.d.ts +15 -0
  141. package/dist/flows/verify-change-verdict.js +25 -0
  142. package/dist/flows/watch-batcher.d.ts +3 -2
  143. package/dist/honesty/already-true.d.ts +2 -0
  144. package/dist/honesty/already-true.js +31 -0
  145. package/dist/honesty/blind-spots.d.ts +35 -3
  146. package/dist/honesty/blind-spots.js +57 -12
  147. package/dist/honesty/honesty.d.ts +3 -2
  148. package/dist/honesty/honesty.js +4 -4
  149. package/dist/honesty/uncaptured-bodies.js +1 -1
  150. package/dist/honesty/unread-outcome.js +2 -2
  151. package/dist/honesty/verified.d.ts +31 -3
  152. package/dist/honesty/verified.js +64 -8
  153. package/dist/http-server.js +23 -7
  154. package/dist/hunt/hunt-report.d.ts +2 -1
  155. package/dist/hunt/hunt-report.js +2 -2
  156. package/dist/index.d.ts +9 -6
  157. package/dist/index.js +41 -25
  158. package/dist/init/agent-rules.d.ts +20 -1
  159. package/dist/init/agent-rules.js +64 -8
  160. package/dist/init/app-choice.d.ts +20 -0
  161. package/dist/init/app-choice.js +25 -0
  162. package/dist/init/astro-host.d.ts +21 -0
  163. package/dist/init/astro-host.js +27 -0
  164. package/dist/init/astro-patch.d.ts +16 -0
  165. package/dist/init/astro-patch.js +240 -0
  166. package/dist/init/capabilities.js +4 -1
  167. package/dist/init/cra.d.ts +50 -0
  168. package/dist/init/cra.js +106 -0
  169. package/dist/init/cursor.js +39 -4
  170. package/dist/init/desktop-doctor.d.ts +3 -2
  171. package/dist/init/desktop-doctor.js +4 -4
  172. package/dist/init/detect.d.ts +9 -2
  173. package/dist/init/detect.js +24 -5
  174. package/dist/init/mcp-clients.d.ts +81 -0
  175. package/dist/init/mcp-clients.js +248 -0
  176. package/dist/init/mcp-registered.d.ts +12 -0
  177. package/dist/init/mcp-registered.js +14 -0
  178. package/dist/init/mcp.d.ts +27 -2
  179. package/dist/init/mcp.js +40 -4
  180. package/dist/init/next-patch.d.ts +2 -4
  181. package/dist/init/next-patch.js +109 -19
  182. package/dist/init/node-io.js +29 -6
  183. package/dist/init/plan-framework.d.ts +16 -1
  184. package/dist/init/plan-framework.js +166 -8
  185. package/dist/init/plan.d.ts +85 -6
  186. package/dist/init/plan.js +196 -33
  187. package/dist/init/project-id.d.ts +5 -9
  188. package/dist/init/project-id.js +10 -22
  189. package/dist/init/run.d.ts +23 -1
  190. package/dist/init/run.js +338 -47
  191. package/dist/init/slash-command.d.ts +9 -0
  192. package/dist/init/slash-command.js +10 -1
  193. package/dist/init/snippets.d.ts +7 -10
  194. package/dist/init/snippets.js +44 -15
  195. package/dist/init/vite-config.js +1 -1
  196. package/dist/init/windows-quote.d.ts +38 -0
  197. package/dist/init/windows-quote.js +70 -0
  198. package/dist/init/workspace-apps.d.ts +23 -0
  199. package/dist/init/workspace-apps.js +68 -0
  200. package/dist/input/driven-redaction.js +1 -1
  201. package/dist/input/network-detail.js +8 -8
  202. package/dist/input/network-mock-tools.js +2 -2
  203. package/dist/input/network-mock.js +4 -4
  204. package/dist/input/real-input.js +8 -3
  205. package/dist/input/scroll-find.js +2 -2
  206. package/dist/input/scroll-tools.d.ts +0 -5
  207. package/dist/input/scroll-tools.js +11 -4
  208. package/dist/input/viewport-tools.js +2 -2
  209. package/dist/journal/ambient-store.d.ts +0 -2
  210. package/dist/journal/ambient-store.js +1 -1
  211. package/dist/journal/ambient.js +1 -1
  212. package/dist/journal/attach-journal.d.ts +2 -1
  213. package/dist/journal/deviation-report.js +6 -6
  214. package/dist/journal/deviation-service.js +1 -1
  215. package/dist/journal/envelope-store.d.ts +0 -2
  216. package/dist/journal/envelope-store.js +1 -1
  217. package/dist/journal/envelope.d.ts +2 -3
  218. package/dist/journal/envelope.js +3 -3
  219. package/dist/journal/journal-query.js +1 -1
  220. package/dist/journal/journal-recorder.d.ts +2 -1
  221. package/dist/journal/journal-recorder.js +2 -2
  222. package/dist/journal/rollups.js +9 -9
  223. package/dist/journal/session-end.d.ts +2 -1
  224. package/dist/journal/session-journal.js +4 -4
  225. package/dist/license/license.d.ts +0 -14
  226. package/dist/license/license.js +8 -8
  227. package/dist/log.d.ts +10 -0
  228. package/dist/log.js +11 -1
  229. package/dist/mcp/mcp-is-error.d.ts +13 -0
  230. package/dist/mcp/mcp-is-error.js +18 -0
  231. package/dist/mcp/mcp-outage.d.ts +32 -0
  232. package/dist/mcp/mcp-outage.js +55 -0
  233. package/dist/mcp/mcp-proxy.d.ts +134 -0
  234. package/dist/mcp/mcp-proxy.js +720 -0
  235. package/dist/{mcp.d.ts → mcp/mcp.d.ts} +13 -10
  236. package/dist/{mcp.js → mcp/mcp.js} +139 -37
  237. package/dist/mcp/proxy-handshake.d.ts +3 -0
  238. package/dist/mcp/proxy-handshake.js +54 -0
  239. package/dist/mcp/proxy-lifecycle.d.ts +64 -0
  240. package/dist/mcp/proxy-lifecycle.js +70 -0
  241. package/dist/mcp/tool-catalog-cache.d.ts +29 -0
  242. package/dist/mcp/tool-catalog-cache.js +55 -0
  243. package/dist/oracles/propose-consequences.d.ts +2 -1
  244. package/dist/oracles/propose-consequences.js +16 -11
  245. package/dist/oracles/self-instrument.d.ts +2 -1
  246. package/dist/oracles/self-instrument.js +1 -1
  247. package/dist/phenomena/phenomena.js +3 -3
  248. package/dist/platform.d.ts +16 -0
  249. package/dist/platform.js +16 -0
  250. package/dist/pool/browser-pool.d.ts +9 -0
  251. package/dist/pool/browser-pool.js +38 -9
  252. package/dist/pool/playwright-launcher.d.ts +15 -2
  253. package/dist/pool/playwright-launcher.js +21 -2
  254. package/dist/project/fs-port.js +1 -1
  255. package/dist/project/project-tools.js +2 -2
  256. package/dist/runs/build-verification-run.js +1 -1
  257. package/dist/runs/repair-prompt.d.ts +0 -6
  258. package/dist/runs/repair-prompt.js +3 -3
  259. package/dist/runs/risk-classify.js +1 -1
  260. package/dist/runs/run-diff.d.ts +0 -2
  261. package/dist/runs/run-diff.js +3 -3
  262. package/dist/runs/run-tools.js +2 -2
  263. package/dist/runs/verification-sync.js +2 -2
  264. package/dist/runs/verify-http.js +1 -1
  265. package/dist/runs/verify-server.js +1 -1
  266. package/dist/session/attachment-history.d.ts +46 -0
  267. package/dist/session/attachment-history.js +65 -0
  268. package/dist/session/command-timeout.d.ts +11 -3
  269. package/dist/session/command-timeout.js +2 -2
  270. package/dist/session/control-envelope.d.ts +6 -0
  271. package/dist/session/control-envelope.js +7 -1
  272. package/dist/session/dev-server-probe.d.ts +13 -0
  273. package/dist/session/dev-server-probe.js +52 -0
  274. package/dist/session/last-act.d.ts +17 -3
  275. package/dist/session/last-act.js +16 -4
  276. package/dist/session/live-control-tools.js +7 -2
  277. package/dist/session/live-control.js +1 -1
  278. package/dist/session/looks-like-dev-server.d.ts +14 -0
  279. package/dist/session/looks-like-dev-server.js +25 -0
  280. package/dist/session/no-session-diagnosis.d.ts +45 -0
  281. package/dist/session/no-session-diagnosis.js +120 -0
  282. package/dist/session/no-session-watch.d.ts +21 -0
  283. package/dist/session/no-session-watch.js +69 -0
  284. package/dist/session/observed-state.d.ts +4 -0
  285. package/dist/session/observed-state.js +19 -1
  286. package/dist/session/output-budget.js +1 -1
  287. package/dist/session/pending-commands.js +1 -0
  288. package/dist/session/session-health.d.ts +8 -1
  289. package/dist/session/session-health.js +8 -7
  290. package/dist/session/session-info.d.ts +43 -8
  291. package/dist/session/session-info.js +46 -1
  292. package/dist/session/session-lease.d.ts +0 -11
  293. package/dist/session/session-lease.js +1 -1
  294. package/dist/session/session-manager.d.ts +6 -0
  295. package/dist/session/session-manager.js +124 -12
  296. package/dist/session/session-readiness.d.ts +1 -1
  297. package/dist/session/session-readiness.js +6 -1
  298. package/dist/session/session-reaper.js +1 -1
  299. package/dist/session/session-reconnect.d.ts +32 -0
  300. package/dist/session/session-reconnect.js +42 -0
  301. package/dist/session/session.d.ts +15 -2
  302. package/dist/session/session.js +77 -26
  303. package/dist/session/state-select.d.ts +1 -1
  304. package/dist/session/state-select.js +1 -1
  305. package/dist/status-payload.d.ts +19 -0
  306. package/dist/status-payload.js +11 -0
  307. package/dist/telemetry/argument-shape.js +10 -1
  308. package/dist/telemetry/browser-mode.d.ts +27 -0
  309. package/dist/telemetry/browser-mode.js +33 -0
  310. package/dist/telemetry/bug-found.d.ts +17 -21
  311. package/dist/telemetry/bug-found.js +120 -8
  312. package/dist/telemetry/cli-telemetry.d.ts +2 -12
  313. package/dist/telemetry/cli-telemetry.js +27 -4
  314. package/dist/telemetry/crash-cause.d.ts +47 -0
  315. package/dist/telemetry/crash-cause.js +104 -0
  316. package/dist/telemetry/daemon-telemetry.d.ts +38 -1
  317. package/dist/telemetry/daemon-telemetry.js +37 -12
  318. package/dist/telemetry/dev-repo.js +4 -4
  319. package/dist/telemetry/error-class.d.ts +26 -0
  320. package/dist/telemetry/error-class.js +52 -0
  321. package/dist/telemetry/error-fingerprint.js +21 -2
  322. package/dist/telemetry/feedback-cli.d.ts +8 -3
  323. package/dist/telemetry/feedback-cli.js +52 -8
  324. package/dist/telemetry/feedback-context.d.ts +23 -6
  325. package/dist/telemetry/feedback-context.js +105 -5
  326. package/dist/telemetry/feedback-delivery.d.ts +20 -0
  327. package/dist/telemetry/feedback-delivery.js +30 -0
  328. package/dist/telemetry/feedback-outbox.d.ts +22 -0
  329. package/dist/telemetry/feedback-outbox.js +87 -0
  330. package/dist/telemetry/feedback.d.ts +22 -1
  331. package/dist/telemetry/feedback.js +67 -8
  332. package/dist/telemetry/git-facts.d.ts +7 -0
  333. package/dist/telemetry/git-facts.js +5 -5
  334. package/dist/telemetry/project-profile.js +2 -1
  335. package/dist/telemetry/session-metrics.d.ts +47 -3
  336. package/dist/telemetry/session-metrics.js +315 -15
  337. package/dist/telemetry/telemetry.d.ts +45 -2
  338. package/dist/telemetry/telemetry.js +150 -37
  339. package/dist/telemetry/verification-of.d.ts +22 -0
  340. package/dist/telemetry/verification-of.js +84 -0
  341. package/dist/tools/act-capsule.js +2 -2
  342. package/dist/tools/act-danger.d.ts +12 -0
  343. package/dist/tools/act-danger.js +20 -3
  344. package/dist/tools/act-helpers.js +4 -4
  345. package/dist/tools/act-tools.d.ts +0 -8
  346. package/dist/tools/act-tools.js +114 -118
  347. package/dist/tools/act-view.js +7 -7
  348. package/dist/tools/alias-args.d.ts +17 -0
  349. package/dist/tools/alias-args.js +25 -0
  350. package/dist/tools/assert-grade.js +9 -9
  351. package/dist/tools/assert-verdict.d.ts +9 -1
  352. package/dist/tools/assert-verdict.js +21 -6
  353. package/dist/tools/browser-tools.js +36 -11
  354. package/dist/tools/contract-tools.d.ts +0 -5
  355. package/dist/tools/contract-tools.js +27 -1
  356. package/dist/tools/coverage-identity.d.ts +34 -0
  357. package/dist/tools/coverage-identity.js +46 -0
  358. package/dist/tools/coverage-tools.js +10 -14
  359. package/dist/tools/dynamic-tools.d.ts +2 -1
  360. package/dist/tools/dynamic-tools.js +96 -19
  361. package/dist/tools/error-recovery.d.ts +34 -2
  362. package/dist/tools/error-recovery.js +255 -4
  363. package/dist/tools/feedback-invite.d.ts +48 -0
  364. package/dist/tools/feedback-invite.js +71 -0
  365. package/dist/tools/feedback-tools.d.ts +12 -1
  366. package/dist/tools/feedback-tools.js +28 -5
  367. package/dist/tools/invoke-tool.d.ts +0 -12
  368. package/dist/tools/invoke-tool.js +126 -33
  369. package/dist/tools/lease-hint.d.ts +17 -0
  370. package/dist/tools/lease-hint.js +24 -0
  371. package/dist/tools/lease-tools.js +6 -5
  372. package/dist/tools/merge-tools.d.ts +10 -1
  373. package/dist/tools/merge-tools.js +9 -3
  374. package/dist/tools/merged-name-redirect.d.ts +13 -0
  375. package/dist/tools/merged-name-redirect.js +55 -0
  376. package/dist/tools/navigate-arrival.d.ts +26 -0
  377. package/dist/tools/navigate-arrival.js +60 -0
  378. package/dist/tools/navigate-result.d.ts +31 -0
  379. package/dist/tools/navigate-result.js +38 -0
  380. package/dist/tools/observe-tools.js +72 -20
  381. package/dist/tools/observed-nothing.d.ts +25 -0
  382. package/dist/tools/observed-nothing.js +33 -0
  383. package/dist/tools/query-paginate.js +2 -2
  384. package/dist/tools/query-shape.d.ts +18 -0
  385. package/dist/tools/query-shape.js +39 -0
  386. package/dist/tools/read-tools.js +81 -20
  387. package/dist/tools/real-input-attempt.d.ts +28 -0
  388. package/dist/tools/real-input-attempt.js +93 -0
  389. package/dist/tools/reconcile-tools.js +7 -7
  390. package/dist/tools/reload-result.d.ts +14 -0
  391. package/dist/tools/reload-result.js +29 -0
  392. package/dist/tools/snapshot-delta.d.ts +4 -5
  393. package/dist/tools/snapshot-delta.js +0 -0
  394. package/dist/tools/tool-kit.d.ts +16 -0
  395. package/dist/tools/tool-surface.d.ts +83 -0
  396. package/dist/tools/tool-surface.js +217 -0
  397. package/dist/tools/tools-helpers.js +6 -6
  398. package/dist/tools/tools.d.ts +15 -0
  399. package/dist/tools/tools.js +53 -17
  400. package/dist/tools/unadvertised-help.d.ts +5 -0
  401. package/dist/tools/unadvertised-help.js +39 -0
  402. package/dist/trace.d.ts +69 -0
  403. package/dist/trace.js +150 -0
  404. package/dist/update/nudge-credit.d.ts +4 -0
  405. package/dist/update/nudge-credit.js +53 -0
  406. package/dist/update/sdk-sync.d.ts +21 -0
  407. package/dist/update/sdk-sync.js +42 -0
  408. package/dist/update/update-checker.js +3 -3
  409. package/dist/update/update-nudge.d.ts +63 -6
  410. package/dist/update/update-nudge.js +128 -15
  411. package/dist/update/updater.d.ts +1 -9
  412. package/dist/update/updater.js +13 -8
  413. package/dist/version/peer-announce.d.ts +4 -0
  414. package/dist/version/peer-announce.js +30 -0
  415. package/dist/{server-version.js → version/server-version.js} +5 -1
  416. package/dist/version/version-nudge.d.ts +28 -0
  417. package/dist/version/version-nudge.js +40 -0
  418. package/dist/version/version-skew.d.ts +66 -0
  419. package/dist/version/version-skew.js +71 -0
  420. package/dist/visual/visual-diff.js +2 -2
  421. package/dist/visual/visual-tools.js +4 -4
  422. package/docs/README.md +55 -0
  423. package/docs/agent-cheatsheet.md +85 -0
  424. package/docs/architecture.md +113 -0
  425. package/docs/benchmarks.md +255 -0
  426. package/docs/debugging.md +102 -0
  427. package/docs/deploy-checks.md +69 -0
  428. package/docs/desktop-apps.md +256 -0
  429. package/docs/enterprise.md +62 -0
  430. package/docs/fixtures-dispatch-receiver.yml +76 -0
  431. package/docs/fixtures.md +45 -0
  432. package/docs/flows.md +166 -0
  433. package/docs/gate-plan.md +98 -0
  434. package/docs/gates.md +117 -0
  435. package/docs/getting-started.md +469 -0
  436. package/docs/human-control.md +63 -0
  437. package/docs/integration-patterns.md +171 -0
  438. package/docs/local-registry.md +126 -0
  439. package/docs/matrix/2.5.0/claude-code-machine.json +19 -0
  440. package/docs/matrix/2.5.0/codex-machine.json +20 -0
  441. package/docs/matrix/2.5.0/cursor-machine.json +22 -0
  442. package/docs/matrix/2.5.0/gemini-machine.json +22 -0
  443. package/docs/matrix/2.5.0/opencode-machine.json +22 -0
  444. package/docs/matrix/2.5.0/vscode-machine.json +22 -0
  445. package/docs/matrix/2.5.0/windsurf-machine.json +22 -0
  446. package/docs/matrix/MATRIX.md +26 -0
  447. package/docs/matrix/README.md +59 -0
  448. package/docs/mint.json +61 -0
  449. package/docs/multi-agent-testing.md +55 -0
  450. package/docs/platform-integration.md +122 -0
  451. package/docs/system-map.md +152 -0
  452. package/docs/telemetry-contract.md +214 -0
  453. package/docs/telemetry.md +191 -0
  454. package/docs/testing.md +82 -0
  455. package/docs/token-efficiency.md +80 -0
  456. package/docs/usage.md +1046 -0
  457. package/package.json +8 -5
  458. package/dist/cli-doctor.d.ts +0 -6
  459. package/dist/cli-doctor.js +0 -60
  460. package/dist/cli-launch.js +0 -112
  461. package/dist/daemon-resilience.d.ts +0 -22
  462. package/dist/daemon-resilience.js +0 -85
  463. package/dist/mcp-proxy.d.ts +0 -67
  464. package/dist/mcp-proxy.js +0 -337
  465. package/dist/tools/profiles.d.ts +0 -37
  466. package/dist/tools/profiles.js +0 -139
  467. /package/dist/{pairing-token.d.ts → bridge/pairing-token.d.ts} +0 -0
  468. /package/dist/{token-auth.d.ts → bridge/token-auth.d.ts} +0 -0
  469. /package/dist/{token-auth.js → bridge/token-auth.js} +0 -0
  470. /package/dist/{cli-verify.d.ts → cli/cli-verify.d.ts} +0 -0
  471. /package/dist/{cloud-cli.d.ts → cli/cloud-cli.d.ts} +0 -0
  472. /package/dist/{server-version.d.ts → version/server-version.d.ts} +0 -0
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Whether this daemon has ever been useful to anyone.
3
+ *
4
+ * Reticle is usually registered as a GLOBAL MCP server, so every agent session in every directory
5
+ * spawns it — including the three quarters of directories that are not web apps at all. Measured
6
+ * over a day: `stack` was null for 92 of 121 profiled projects, 50 were not even git repositories,
7
+ * and 74% of daemon sessions never called a single tool. Median daemon lifetime 28 minutes; total
8
+ * uptime across those sessions 10,816 minutes against 4.6 minutes of actual work — a duty cycle of
9
+ * **0.04%**.
10
+ *
11
+ * The existing idle-shutdown could never fire on any of it, because its predicate is
12
+ * `!agentConnected && …` and an attached agent keeps `agentConnected` true for the whole editor
13
+ * session. So a daemon sat for half an hour, in a directory with no app, doing nothing, holding
14
+ * memory and a port.
15
+ *
16
+ * The rule here is deliberately the most conservative one that still catches that population:
17
+ * a daemon is USELESS only if it has never served a tool call AND no browser has ever connected to
18
+ * it AND nothing is leased from the pool. All three are lifetime facts, not windows — so nothing
19
+ * transient trips it, and a daemon in this state provably holds no recording, baseline or session
20
+ * anybody could lose. When it does shut down, the MCP proxy reconnects and `ensureDaemon` spawns a
21
+ * fresh one on the next call, which is a path already exercised hundreds of times a day.
22
+ */
23
+ /** Set the first time any tool is dispatched through runTool — the chokepoint every path shares. */
24
+ let servedToolCall = false;
25
+ export function noteToolCall() {
26
+ servedToolCall = true;
27
+ }
28
+ export function everServedToolCall() {
29
+ return servedToolCall;
30
+ }
31
+ /** Tests only. */
32
+ export function resetDaemonUsefulness() {
33
+ servedToolCall = false;
34
+ }
35
+ /**
36
+ * True when the daemon has done nothing for anyone and has nothing to lose by exiting.
37
+ *
38
+ * Every clause is a LIFETIME fact. "No tool call in the last five minutes" would fire on a thinking
39
+ * pause; "no tool call ever" cannot.
40
+ */
41
+ export function isUselessDaemon(facts) {
42
+ return !facts.servedToolCall && !facts.everConnected && 0 === facts.activeLeases;
43
+ }
44
+ /**
45
+ * The daemon's whole "is anything happening" question.
46
+ *
47
+ * Two ways to be idle. The original — nobody attached, nothing connected. And the one that was
48
+ * missing: an agent IS attached but this daemon has never served a tool call and no browser has ever
49
+ * connected to it. `agentConnected` stays true for an entire editor session, so without the second
50
+ * clause the shutdown watcher could never fire on the 74% of daemons that spawn in a directory with
51
+ * no app and sit there for half an hour.
52
+ */
53
+ export function buildIdlePredicate(agentConnected, sessions, pool) {
54
+ return () => (!agentConnected() ||
55
+ isUselessDaemon({
56
+ servedToolCall: everServedToolCall(),
57
+ everConnected: sessions.everConnected(),
58
+ activeLeases: pool.activeCount(),
59
+ })) &&
60
+ 0 === sessions.count() &&
61
+ 0 === pool.activeCount();
62
+ }
@@ -1,4 +1,34 @@
1
+ /** Env override for the whole state directory — see ReticleEnv.STATE_DIR. */
2
+ export declare const STATE_DIR_ENV: "RETICLE_STATE_DIR";
3
+ /**
4
+ * Where the daemon keeps pidfiles, the discovery registry and its logs.
5
+ *
6
+ * `~/.reticle` unless overridden. The override exists because a read-only $HOME is not hypothetical
7
+ * — a sandboxed agent or a locked-down Windows profile cannot write there, and until now that meant
8
+ * the daemon could not start at all. Read per call rather than captured at import, so a test (and a
9
+ * wrapper that sets it late) sees the current value.
10
+ */
11
+ export declare function reticleStateHome(): string;
1
12
  export declare function logPath(port: number): string;
13
+ /**
14
+ * How large a daemon log may get before it is rolled over to `<name>.1`.
15
+ *
16
+ * It was unbounded. A real dev machine reached **24MB** on one port, which is not merely untidy: it
17
+ * is the difference between a log somebody opens and a log somebody gives up on, and it is disk that
18
+ * nothing ever reclaims. One previous generation is kept, because the question people bring to this
19
+ * file ("what happened just now?") is answered by the current one and the question they bring next
20
+ * ("and just before that?") is answered by the other.
21
+ */
22
+ export declare const MAX_DAEMON_LOG_BYTES: number;
23
+ /**
24
+ * Roll the log over if it has grown past the cap. Best-effort: a daemon must still start when the
25
+ * log cannot be rotated, because failing to launch over housekeeping is strictly worse than a large
26
+ * file.
27
+ */
28
+ export declare function rotateDaemonLog(path: string, deps: {
29
+ fileSize(p: string): number;
30
+ renameFile(from: string, to: string): void;
31
+ }, max?: number): boolean;
2
32
  export declare function readPid(port: number, home?: string): number | null;
3
33
  export declare function isAlive(pid: number): boolean;
4
34
  export declare function writePid(port: number): void;
@@ -20,13 +50,6 @@ export declare function writeDaemonRegistry(port: number, meta: {
20
50
  projectId?: string;
21
51
  startedAt: number;
22
52
  }): void;
23
- export declare function removeDaemonRegistry(port: number): void;
24
- /**
25
- * Discover the port of a live daemon serving `projectId` by reading the registry entries in ~/.reticle.
26
- * Returns null when none matches (caller falls back to the default port — never guesses a mismatched
27
- * daemon). Stale entries (crashed daemons) are ignored via the pid liveness probe.
28
- */
29
- export declare function discoverDaemonPortForProject(projectId: string | undefined): number | null;
30
53
  export declare function isRunning(port: number): boolean;
31
54
  /**
32
55
  * Find the port of a live reticle daemon by scanning ~/.reticle/daemon-<port>.pid files — so `reticle open`
@@ -52,13 +75,15 @@ export interface SpawnDaemonDeps {
52
75
  readonly home: string;
53
76
  openFile(path: string, flags: string): number;
54
77
  closeFile(fd: number): void;
78
+ /** Size in bytes, or 0 when the file does not exist yet. */
79
+ fileSize(path: string): number;
80
+ renameFile(from: string, to: string): void;
55
81
  spawnChild(command: string, args: readonly string[], options: {
56
82
  detached: boolean;
57
83
  stdio: readonly ('ignore' | number)[];
58
84
  }): SpawnedChild;
59
85
  pidAlive(pid: number): boolean;
60
86
  }
61
- export declare function defaultSpawnDaemonDeps(): SpawnDaemonDeps;
62
87
  /**
63
88
  * Spawn the reticle daemon as a detached background process, redirecting output to the log file.
64
89
  * Writes the PID file from the parent before returning so callers can call isRunning
@@ -1,19 +1,62 @@
1
1
  import { join } from 'node:path';
2
2
  import { homedir } from 'node:os';
3
- import { mkdirSync, readFileSync, writeFileSync, existsSync, unlinkSync, openSync, closeSync, readdirSync, } from 'node:fs';
3
+ import { mkdirSync, readFileSync, writeFileSync, existsSync, unlinkSync, openSync, closeSync, readdirSync, statSync, renameSync, writeSync, } from 'node:fs';
4
4
  import { spawn } from 'node:child_process';
5
- import { daemonRegistryFileName, daemonRegistryPort, DaemonRegistryEntrySchema, pickDaemonPort, } from '@reticlehq/core';
6
- const RETICLE_HOME = join(homedir(), '.reticle');
7
- function pidPath(port, home = RETICLE_HOME) {
5
+ import { daemonRegistryFileName, ReticleEnv } from '@reticlehq/core';
6
+ import { log } from '../log.js';
7
+ import { uncleanPredecessor } from './unclean-predecessor.js';
8
+ /** Env override for the whole state directory — see ReticleEnv.STATE_DIR. */
9
+ export const STATE_DIR_ENV = ReticleEnv.STATE_DIR;
10
+ /**
11
+ * Where the daemon keeps pidfiles, the discovery registry and its logs.
12
+ *
13
+ * `~/.reticle` unless overridden. The override exists because a read-only $HOME is not hypothetical
14
+ * — a sandboxed agent or a locked-down Windows profile cannot write there, and until now that meant
15
+ * the daemon could not start at all. Read per call rather than captured at import, so a test (and a
16
+ * wrapper that sets it late) sees the current value.
17
+ */
18
+ export function reticleStateHome() {
19
+ const override = process.env[STATE_DIR_ENV];
20
+ if (override !== undefined && override.length > 0)
21
+ return override;
22
+ return join(homedir(), '.reticle');
23
+ }
24
+ function pidPath(port, home = reticleStateHome()) {
8
25
  return join(home, `daemon-${port}.pid`);
9
26
  }
10
27
  function registryPath(port) {
11
- return join(RETICLE_HOME, daemonRegistryFileName(port));
28
+ return join(reticleStateHome(), daemonRegistryFileName(port));
12
29
  }
13
30
  export function logPath(port) {
14
- return join(RETICLE_HOME, `daemon-${port}.log`);
31
+ return join(reticleStateHome(), `daemon-${port}.log`);
15
32
  }
16
- export function readPid(port, home = RETICLE_HOME) {
33
+ /**
34
+ * How large a daemon log may get before it is rolled over to `<name>.1`.
35
+ *
36
+ * It was unbounded. A real dev machine reached **24MB** on one port, which is not merely untidy: it
37
+ * is the difference between a log somebody opens and a log somebody gives up on, and it is disk that
38
+ * nothing ever reclaims. One previous generation is kept, because the question people bring to this
39
+ * file ("what happened just now?") is answered by the current one and the question they bring next
40
+ * ("and just before that?") is answered by the other.
41
+ */
42
+ export const MAX_DAEMON_LOG_BYTES = 8 * 1024 * 1024;
43
+ /**
44
+ * Roll the log over if it has grown past the cap. Best-effort: a daemon must still start when the
45
+ * log cannot be rotated, because failing to launch over housekeeping is strictly worse than a large
46
+ * file.
47
+ */
48
+ export function rotateDaemonLog(path, deps, max = MAX_DAEMON_LOG_BYTES) {
49
+ try {
50
+ if (max >= deps.fileSize(path))
51
+ return false;
52
+ deps.renameFile(path, `${path}.1`);
53
+ return true;
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ export function readPid(port, home = reticleStateHome()) {
17
60
  const path = pidPath(port, home);
18
61
  if (!existsSync(path))
19
62
  return null;
@@ -30,7 +73,7 @@ export function isAlive(pid) {
30
73
  }
31
74
  }
32
75
  export function writePid(port) {
33
- mkdirSync(RETICLE_HOME, { recursive: true });
76
+ mkdirSync(reticleStateHome(), { recursive: true });
34
77
  writeFileSync(pidPath(port), String(process.pid), 'utf8');
35
78
  }
36
79
  /**
@@ -39,7 +82,7 @@ export function writePid(port) {
39
82
  * the orphan-race guard: a losing childB (EADDRINUSE) must not delete the winning childA's live pidfile.
40
83
  */
41
84
  export function shouldRemovePid(owner, expectedPid, alive) {
42
- return owner === null || owner === expectedPid || !alive;
85
+ return null === owner || owner === expectedPid || !alive;
43
86
  }
44
87
  export function removePid(port, expectedPid = process.pid) {
45
88
  const path = pidPath(port);
@@ -66,14 +109,14 @@ export function writeDaemonRegistry(port, meta) {
66
109
  ...(meta.projectId !== undefined ? { projectId: meta.projectId } : {}),
67
110
  };
68
111
  try {
69
- mkdirSync(RETICLE_HOME, { recursive: true });
112
+ mkdirSync(reticleStateHome(), { recursive: true });
70
113
  writeFileSync(registryPath(port), JSON.stringify(entry), 'utf8');
71
114
  }
72
115
  catch {
73
116
  // discovery is a convenience — never block startup on it
74
117
  }
75
118
  }
76
- export function removeDaemonRegistry(port) {
119
+ function removeDaemonRegistry(port) {
77
120
  const path = registryPath(port);
78
121
  try {
79
122
  if (existsSync(path))
@@ -83,34 +126,6 @@ export function removeDaemonRegistry(port) {
83
126
  // racing another cleaner — already gone
84
127
  }
85
128
  }
86
- /**
87
- * Discover the port of a live daemon serving `projectId` by reading the registry entries in ~/.reticle.
88
- * Returns null when none matches (caller falls back to the default port — never guesses a mismatched
89
- * daemon). Stale entries (crashed daemons) are ignored via the pid liveness probe.
90
- */
91
- export function discoverDaemonPortForProject(projectId) {
92
- const entries = [];
93
- let files;
94
- try {
95
- files = readdirSync(RETICLE_HOME);
96
- }
97
- catch {
98
- return null; // no ~/.reticle yet
99
- }
100
- for (const file of files) {
101
- if (daemonRegistryPort(file) === null)
102
- continue;
103
- try {
104
- const parsed = DaemonRegistryEntrySchema.safeParse(JSON.parse(readFileSync(join(RETICLE_HOME, file), 'utf8')));
105
- if (parsed.success)
106
- entries.push(parsed.data);
107
- }
108
- catch {
109
- // unreadable/corrupt entry — skip it
110
- }
111
- }
112
- return pickDaemonPort(entries, projectId, isAlive);
113
- }
114
129
  export function isRunning(port) {
115
130
  const pid = readPid(port);
116
131
  return pid !== null && isAlive(pid);
@@ -124,12 +139,12 @@ export function discoverDaemonPort() {
124
139
  reclaimStaleDaemons(); // sweep crashed daemons' stale pidfiles before scanning for live ones
125
140
  let found = null;
126
141
  try {
127
- for (const file of readdirSync(RETICLE_HOME)) {
142
+ for (const file of readdirSync(reticleStateHome())) {
128
143
  const m = /^daemon-(\d+)\.pid$/.exec(file);
129
- if (m === null)
144
+ if (null === m)
130
145
  continue;
131
146
  const port = Number(m[1]);
132
- if (isRunning(port) && (found === null || port < found))
147
+ if (isRunning(port) && (null === found || port < found))
133
148
  found = port;
134
149
  }
135
150
  }
@@ -144,7 +159,7 @@ export function discoverDaemonPort() {
144
159
  * Returns the ports reclaimed. `home` and `pidAlive` are injectable for testing (default to the real
145
160
  * ~/.reticle and the process.kill(pid,0) liveness probe).
146
161
  */
147
- export function reclaimStaleDaemons(home = RETICLE_HOME, pidAlive = isAlive) {
162
+ export function reclaimStaleDaemons(home = reticleStateHome(), pidAlive = isAlive) {
148
163
  const reclaimed = [];
149
164
  let files;
150
165
  try {
@@ -155,7 +170,7 @@ export function reclaimStaleDaemons(home = RETICLE_HOME, pidAlive = isAlive) {
155
170
  }
156
171
  for (const file of files) {
157
172
  const match = /^daemon-(\d+)\.pid$/.exec(file);
158
- if (match === null)
173
+ if (null === match)
159
174
  continue;
160
175
  const path = join(home, file);
161
176
  let pid = null;
@@ -167,7 +182,7 @@ export function reclaimStaleDaemons(home = RETICLE_HOME, pidAlive = isAlive) {
167
182
  catch {
168
183
  pid = null; // unreadable pidfile counts as stale
169
184
  }
170
- if (pid === null || !pidAlive(pid)) {
185
+ if (null === pid || !pidAlive(pid)) {
171
186
  try {
172
187
  unlinkSync(path);
173
188
  removeDaemonRegistry(Number(match[1])); // drop the sidecar discovery entry too
@@ -180,11 +195,13 @@ export function reclaimStaleDaemons(home = RETICLE_HOME, pidAlive = isAlive) {
180
195
  }
181
196
  return reclaimed;
182
197
  }
183
- export function defaultSpawnDaemonDeps() {
198
+ function defaultSpawnDaemonDeps() {
184
199
  return {
185
- home: RETICLE_HOME,
200
+ home: reticleStateHome(),
186
201
  openFile: openSync,
187
202
  closeFile: closeSync,
203
+ fileSize: (path) => (existsSync(path) ? statSync(path).size : 0),
204
+ renameFile: renameSync,
188
205
  spawnChild: (command, args, options) => spawn(command, [...args], { detached: options.detached, stdio: [...options.stdio] }),
189
206
  pidAlive: isAlive,
190
207
  };
@@ -198,12 +215,27 @@ export function defaultSpawnDaemonDeps() {
198
215
  * pattern as reclaimStaleDaemons(home, pidAlive).
199
216
  */
200
217
  export function spawnDaemon(nodeExec, scriptPath, args, port, deps = defaultSpawnDaemonDeps()) {
201
- mkdirSync(deps.home, { recursive: true });
218
+ try {
219
+ mkdirSync(deps.home, { recursive: true });
220
+ }
221
+ catch (err) {
222
+ // The log-file open below has always been guarded; this was not, so the ONE failure a sandboxed
223
+ // agent actually hits — a read-only $HOME — escaped as a raw EACCES naming nothing. Refuse
224
+ // loudly, and name the way out.
225
+ log('reticle_daemon_state_unwritable', {
226
+ home: deps.home,
227
+ error: err instanceof Error ? err.message : String(err),
228
+ fix: `Reticle could not create its state directory. Set ${STATE_DIR_ENV} to a writable path and retry.`,
229
+ });
230
+ return false;
231
+ }
202
232
  const pidFilePath = join(deps.home, `daemon-${port}.pid`);
203
233
  const logFilePath = join(deps.home, `daemon-${port}.log`);
204
234
  // O_EXCL spawn-lock: only the FIRST racer to create the pidfile spawns. A concurrent second gets
205
235
  // EEXIST — if a LIVE daemon owns the port it skips (no duplicate detached daemon, no clobbered pid);
206
236
  // a stale pidfile from a crashed daemon is reclaimed. Returns false when it did not spawn.
237
+ /** Set when this start reclaimed a pidfile whose owner is gone — see unclean-predecessor.ts. */
238
+ let orphanPid = null;
207
239
  let lockFd;
208
240
  try {
209
241
  lockFd = deps.openFile(pidFilePath, 'wx');
@@ -212,6 +244,10 @@ export function spawnDaemon(nodeExec, scriptPath, args, port, deps = defaultSpaw
212
244
  const existing = readPid(port, deps.home);
213
245
  if (existing !== null && deps.pidAlive(existing))
214
246
  return false;
247
+ // This branch IS the crash detection — its own comment has always said so ("a stale pidfile from
248
+ // a crashed daemon is reclaimed"), it just never said it out loud. Captured before the unlink,
249
+ // because after it there is no trace left that the previous daemon died at all.
250
+ orphanPid = uncleanPredecessor(existing, false, process.pid);
215
251
  try {
216
252
  unlinkSync(pidFilePath);
217
253
  lockFd = deps.openFile(pidFilePath, 'wx');
@@ -220,6 +256,9 @@ export function spawnDaemon(nodeExec, scriptPath, args, port, deps = defaultSpaw
220
256
  return false; // lost a concurrent reclaim race
221
257
  }
222
258
  }
259
+ // Roll the log BEFORE opening the append handle, or the daemon writes into the renamed file
260
+ // through an fd that no longer matches any name anyone can find.
261
+ rotateDaemonLog(logFilePath, deps);
223
262
  let logFd;
224
263
  try {
225
264
  logFd = deps.openFile(logFilePath, 'a');
@@ -235,6 +274,24 @@ export function spawnDaemon(nodeExec, scriptPath, args, port, deps = defaultSpaw
235
274
  }
236
275
  return false;
237
276
  }
277
+ // Into the daemon log itself, not the parent's stdout: `doctor` points the reader at this file, and
278
+ // a death recorded anywhere else is a death they will not find. Written through the fd the child is
279
+ // about to inherit, so it lands immediately before that child's first line — exactly where a reader
280
+ // comparing two `mcp_daemon_started` entries is already looking.
281
+ if (null !== orphanPid) {
282
+ try {
283
+ writeSync(logFd, `${JSON.stringify({
284
+ // Same convention as log(): the clock is read at the I/O boundary, not injected.
285
+ t: new Date().toISOString(),
286
+ event: 'reticle_daemon_previous_died_unclean',
287
+ port,
288
+ pid: orphanPid,
289
+ })}\n`);
290
+ }
291
+ catch {
292
+ // A log we cannot write is not a reason to refuse to start the daemon.
293
+ }
294
+ }
238
295
  let child;
239
296
  try {
240
297
  child = deps.spawnChild(nodeExec, [scriptPath, ...args], {
@@ -0,0 +1,115 @@
1
+ /**
2
+ * A regular beat, so SILENCE in the daemon log becomes evidence.
3
+ *
4
+ * `installExitTrace` hooks `'exit'`, which a SIGKILL never fires. So a daemon that is killed —
5
+ * crashed, OOM-ed, swept by another project, `kill -9`'d by the recipe in every troubleshooting
6
+ * thread — leaves no trace at all, and the last line before its port goes dark is whatever it
7
+ * happened to be doing. Worse, a daemon that DID exit logs `code: 0`, so a tidy shutdown and a
8
+ * violent death are the same shape to anyone reading the file.
9
+ *
10
+ * Measured cost of that: a correct SvelteKit install was written up as an install failure, because
11
+ * the daemon had died 21 seconds earlier and nothing in the log said so. The fixture was named in the
12
+ * report; the daemon was not.
13
+ *
14
+ * Two halves, and the second is what makes the first worth having:
15
+ * - DaemonHeartbeat emits `reticle_daemon_alive` on a fixed cadence, unconditionally.
16
+ * - classifyDaemonLife reads a log back and says which way the daemon ended.
17
+ *
18
+ * A heartbeat nobody interprets is just log volume.
19
+ */
20
+ /** The event name a beat carries. One `grep` separates liveness from everything else in the file. */
21
+ export declare const DAEMON_HEARTBEAT_EVENT = "reticle_daemon_alive";
22
+ /** Written by installExitTrace when the process exits through Node. */
23
+ export declare const DAEMON_EXIT_EVENT = "reticle_daemon_exiting";
24
+ /** Written by installExitTrace when a signal arrives, which is a CAUSE the exit line does not carry. */
25
+ export declare const DAEMON_SIGNAL_EVENT = "reticle_daemon_signalled";
26
+ /**
27
+ * How often a live daemon says so.
28
+ *
29
+ * The median daemon lives 28 minutes, so at 30s that is ~56 lines — readable, and small against a log
30
+ * that already rotates. Shorter would make the gap sharper and the file noisier; the gap only needs
31
+ * to be legible against a connect window, which is measured in seconds.
32
+ */
33
+ export declare const DAEMON_HEARTBEAT_MS = 30000;
34
+ /**
35
+ * The cadence, from the environment. Same shape as `resolveIdleCheckMs`: a bad value falls back
36
+ * rather than throwing, and 0 or negative is not a legal cadence — it would spin.
37
+ */
38
+ export declare function resolveHeartbeatMs(raw: string | undefined): number;
39
+ interface HeartbeatFacts {
40
+ /** Browser sessions connected right now. */
41
+ sessions: number;
42
+ /**
43
+ * Has this daemon EVER served a tool call? A lifetime fact, not a window count.
44
+ *
45
+ * It is the difference between a death that cost somebody work and one that cost nothing — the
46
+ * same distinction `isUselessDaemon` already draws. A windowed count would reset under the reader
47
+ * and make a long-lived daemon look freshly idle.
48
+ */
49
+ served: boolean;
50
+ }
51
+ interface DaemonHeartbeatOptions {
52
+ log: (event: string, fields: Record<string, unknown>) => void;
53
+ /** Read at each beat, never cached — the point is to describe the daemon NOW. */
54
+ facts: () => HeartbeatFacts;
55
+ intervalMs?: number;
56
+ /** Injected, per the repo rule: never call Date.now() inside logic. */
57
+ clock?: () => number;
58
+ }
59
+ export declare class DaemonHeartbeat {
60
+ #private;
61
+ constructor(opts: DaemonHeartbeatOptions);
62
+ /**
63
+ * One beat. Exposed so a test can drive it with an injected clock rather than waiting.
64
+ *
65
+ * Unconditional on purpose. Skipping the beat when nothing happened is the obvious optimisation and
66
+ * it destroys the signal: silence would then mean "idle" OR "dead", which is precisely the
67
+ * ambiguity this exists to remove.
68
+ */
69
+ beat(): void;
70
+ start(): void;
71
+ stop(): void;
72
+ }
73
+ /** How a daemon's life ended, as far as its own log can say. */
74
+ export declare const DaemonEnd: {
75
+ /** Still beating within tolerance. */
76
+ readonly ALIVE: "alive";
77
+ /** Logged its own exit, with no signal before it. */
78
+ readonly CLEAN: "clean";
79
+ /** A signal arrived — the cause an exit line with `code: 0` does not carry. */
80
+ readonly SIGNALLED: "signalled";
81
+ /** The beat simply stopped. No exit, no signal: killed, crashed, or swept. */
82
+ readonly DIED_SILENTLY: "died_silently";
83
+ /** No beat was ever emitted, so this log cannot answer the question. */
84
+ readonly UNKNOWN: "unknown";
85
+ };
86
+ export type DaemonEnd = (typeof DaemonEnd)[keyof typeof DaemonEnd];
87
+ /**
88
+ * One parsed line of a daemon log. The index signature is not laziness: these lines carry whatever
89
+ * their event needed (`code`, `port`, `pid`, `reason`, …), and a reader that refused unknown keys
90
+ * would reject the very file it exists to read.
91
+ */
92
+ interface LogLine {
93
+ t: number;
94
+ event: string;
95
+ signal?: unknown;
96
+ everyMs?: unknown;
97
+ [field: string]: unknown;
98
+ }
99
+ export interface DaemonLife {
100
+ end: DaemonEnd;
101
+ /** When the last beat landed, or undefined if there was never one. */
102
+ lastBeatMs?: number;
103
+ /** How long the log has been silent, when that is the finding. */
104
+ silentForMs?: number;
105
+ signal?: string;
106
+ }
107
+ /**
108
+ * Read a daemon log back and say how it ended.
109
+ *
110
+ * UNKNOWN rather than ALIVE when there is no heartbeat at all — every log written before this
111
+ * existed, and any file the reader was pointed at by mistake. Inferring health from the absence of a
112
+ * signal we never sent is the same confident-wrong this module replaces.
113
+ */
114
+ export declare function classifyDaemonLife(lines: readonly LogLine[], nowMs: number): DaemonLife;
115
+ export {};
@@ -0,0 +1,141 @@
1
+ /**
2
+ * A regular beat, so SILENCE in the daemon log becomes evidence.
3
+ *
4
+ * `installExitTrace` hooks `'exit'`, which a SIGKILL never fires. So a daemon that is killed —
5
+ * crashed, OOM-ed, swept by another project, `kill -9`'d by the recipe in every troubleshooting
6
+ * thread — leaves no trace at all, and the last line before its port goes dark is whatever it
7
+ * happened to be doing. Worse, a daemon that DID exit logs `code: 0`, so a tidy shutdown and a
8
+ * violent death are the same shape to anyone reading the file.
9
+ *
10
+ * Measured cost of that: a correct SvelteKit install was written up as an install failure, because
11
+ * the daemon had died 21 seconds earlier and nothing in the log said so. The fixture was named in the
12
+ * report; the daemon was not.
13
+ *
14
+ * Two halves, and the second is what makes the first worth having:
15
+ * - DaemonHeartbeat emits `reticle_daemon_alive` on a fixed cadence, unconditionally.
16
+ * - classifyDaemonLife reads a log back and says which way the daemon ended.
17
+ *
18
+ * A heartbeat nobody interprets is just log volume.
19
+ */
20
+ /** The event name a beat carries. One `grep` separates liveness from everything else in the file. */
21
+ export const DAEMON_HEARTBEAT_EVENT = 'reticle_daemon_alive';
22
+ /** Written by installExitTrace when the process exits through Node. */
23
+ export const DAEMON_EXIT_EVENT = 'reticle_daemon_exiting';
24
+ /** Written by installExitTrace when a signal arrives, which is a CAUSE the exit line does not carry. */
25
+ export const DAEMON_SIGNAL_EVENT = 'reticle_daemon_signalled';
26
+ /**
27
+ * How often a live daemon says so.
28
+ *
29
+ * The median daemon lives 28 minutes, so at 30s that is ~56 lines — readable, and small against a log
30
+ * that already rotates. Shorter would make the gap sharper and the file noisier; the gap only needs
31
+ * to be legible against a connect window, which is measured in seconds.
32
+ */
33
+ export const DAEMON_HEARTBEAT_MS = 30_000;
34
+ /**
35
+ * How many beats may be missed before a reader calls it dead.
36
+ *
37
+ * Timers slip under load — the battery boots three servers and a browser — and a reader that cries
38
+ * death on one late beat is a reader people learn to ignore. Two intervals plus a margin.
39
+ */
40
+ const MISSED_BEATS_TOLERATED = 2;
41
+ /**
42
+ * The cadence, from the environment. Same shape as `resolveIdleCheckMs`: a bad value falls back
43
+ * rather than throwing, and 0 or negative is not a legal cadence — it would spin.
44
+ */
45
+ export function resolveHeartbeatMs(raw) {
46
+ if (raw === undefined || '' === raw.trim())
47
+ return DAEMON_HEARTBEAT_MS;
48
+ const n = Number(raw);
49
+ if (!Number.isFinite(n) || n <= 0)
50
+ return DAEMON_HEARTBEAT_MS;
51
+ return Math.floor(n);
52
+ }
53
+ export class DaemonHeartbeat {
54
+ #log;
55
+ #facts;
56
+ #intervalMs;
57
+ #clock;
58
+ #startedAt;
59
+ #timer;
60
+ constructor(opts) {
61
+ this.#log = opts.log;
62
+ this.#facts = opts.facts;
63
+ this.#intervalMs = opts.intervalMs ?? DAEMON_HEARTBEAT_MS;
64
+ this.#clock = opts.clock ?? (() => Date.now());
65
+ this.#startedAt = this.#clock();
66
+ }
67
+ /**
68
+ * One beat. Exposed so a test can drive it with an injected clock rather than waiting.
69
+ *
70
+ * Unconditional on purpose. Skipping the beat when nothing happened is the obvious optimisation and
71
+ * it destroys the signal: silence would then mean "idle" OR "dead", which is precisely the
72
+ * ambiguity this exists to remove.
73
+ */
74
+ beat() {
75
+ const facts = this.#facts();
76
+ this.#log(DAEMON_HEARTBEAT_EVENT, {
77
+ uptimeMs: this.#clock() - this.#startedAt,
78
+ // The cadence travels WITH the beat so a reader never hard-codes it. Hard-coding is how the
79
+ // reader and the emitter drift apart the first time the interval is tuned.
80
+ everyMs: this.#intervalMs,
81
+ sessions: facts.sessions,
82
+ served: facts.served,
83
+ });
84
+ }
85
+ start() {
86
+ if (this.#timer !== undefined)
87
+ return;
88
+ this.#timer = setInterval(() => this.beat(), this.#intervalMs);
89
+ // Never the reason a process stays alive — this is a diagnostic, not a workload.
90
+ this.#timer.unref();
91
+ }
92
+ stop() {
93
+ if (this.#timer === undefined)
94
+ return;
95
+ clearInterval(this.#timer);
96
+ this.#timer = undefined;
97
+ }
98
+ }
99
+ /** How a daemon's life ended, as far as its own log can say. */
100
+ export const DaemonEnd = {
101
+ /** Still beating within tolerance. */
102
+ ALIVE: 'alive',
103
+ /** Logged its own exit, with no signal before it. */
104
+ CLEAN: 'clean',
105
+ /** A signal arrived — the cause an exit line with `code: 0` does not carry. */
106
+ SIGNALLED: 'signalled',
107
+ /** The beat simply stopped. No exit, no signal: killed, crashed, or swept. */
108
+ DIED_SILENTLY: 'died_silently',
109
+ /** No beat was ever emitted, so this log cannot answer the question. */
110
+ UNKNOWN: 'unknown',
111
+ };
112
+ /**
113
+ * Read a daemon log back and say how it ended.
114
+ *
115
+ * UNKNOWN rather than ALIVE when there is no heartbeat at all — every log written before this
116
+ * existed, and any file the reader was pointed at by mistake. Inferring health from the absence of a
117
+ * signal we never sent is the same confident-wrong this module replaces.
118
+ */
119
+ export function classifyDaemonLife(lines, nowMs) {
120
+ const beats = lines.filter((l) => l.event === DAEMON_HEARTBEAT_EVENT);
121
+ const exit = lines.findLast((l) => l.event === DAEMON_EXIT_EVENT);
122
+ const signal = lines.findLast((l) => l.event === DAEMON_SIGNAL_EVENT);
123
+ if (exit !== undefined) {
124
+ // A signal is the CAUSE; the exit line only says it happened. Reporting `clean` for a SIGTERMed
125
+ // daemon is how "shut down tidily" and "the bridge every app needs is gone" became one sentence.
126
+ return signal === undefined
127
+ ? { end: DaemonEnd.CLEAN }
128
+ : {
129
+ end: DaemonEnd.SIGNALLED,
130
+ ...('string' === typeof signal.signal ? { signal: signal.signal } : {}),
131
+ };
132
+ }
133
+ const last = beats.at(-1);
134
+ if (last === undefined)
135
+ return { end: DaemonEnd.UNKNOWN };
136
+ const everyMs = 'number' === typeof last.everyMs ? last.everyMs : DAEMON_HEARTBEAT_MS;
137
+ const silentForMs = nowMs - last.t;
138
+ return silentForMs > everyMs * MISSED_BEATS_TOLERATED
139
+ ? { end: DaemonEnd.DIED_SILENTLY, lastBeatMs: last.t, silentForMs }
140
+ : { end: DaemonEnd.ALIVE, lastBeatMs: last.t };
141
+ }