@reticlehq/server 2.4.0 → 2.5.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 (416) hide show
  1. package/SKILL.md +702 -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} +1 -1
  7. package/dist/{bridge.js → bridge/bridge.js} +63 -13
  8. package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +1 -11
  9. package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +9 -9
  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-doctor.js → cli/cli-doctor.js} +9 -2
  20. package/dist/{cli-flow-commands.d.ts → cli/cli-flow-commands.d.ts} +2 -2
  21. package/dist/{cli-flow-commands.js → cli/cli-flow-commands.js} +15 -15
  22. package/dist/{cli-launch.d.ts → cli/cli-launch.d.ts} +32 -3
  23. package/dist/cli/cli-launch.js +171 -0
  24. package/dist/{cli-parse.d.ts → cli/cli-parse.d.ts} +2 -11
  25. package/dist/{cli-parse.js → cli/cli-parse.js} +113 -60
  26. package/dist/{cli-port.d.ts → cli/cli-port.d.ts} +12 -0
  27. package/dist/{cli-port.js → cli/cli-port.js} +9 -9
  28. package/dist/cli/cli-update-commands.d.ts +4 -0
  29. package/dist/cli/cli-update-commands.js +93 -0
  30. package/dist/{cli-verify.js → cli/cli-verify.js} +14 -14
  31. package/dist/{cloud-cli.js → cli/cloud-cli.js} +29 -24
  32. package/dist/cli.d.ts +2 -2
  33. package/dist/cli.js +174 -92
  34. package/dist/cloud/cloud-config.d.ts +0 -1
  35. package/dist/cloud/cloud-config.js +9 -9
  36. package/dist/cloud/cloud-sync.d.ts +1 -7
  37. package/dist/cloud/cloud-sync.js +12 -12
  38. package/dist/crawl/crawl-empty.d.ts +16 -0
  39. package/dist/crawl/crawl-empty.js +24 -0
  40. package/dist/crawl/crawl-tools.js +4 -1
  41. package/dist/crawl/crawl.js +26 -7
  42. package/dist/daemon/daemon-resilience.d.ts +49 -0
  43. package/dist/daemon/daemon-resilience.js +191 -0
  44. package/dist/daemon/daemon-usefulness.d.ts +61 -0
  45. package/dist/daemon/daemon-usefulness.js +62 -0
  46. package/dist/{daemon.d.ts → daemon/daemon.d.ts} +33 -8
  47. package/dist/{daemon.js → daemon/daemon.js} +80 -48
  48. package/dist/daemon/idle-grace.d.ts +32 -0
  49. package/dist/daemon/idle-grace.js +41 -0
  50. package/dist/{idle-shutdown.d.ts → daemon/idle-shutdown.d.ts} +13 -0
  51. package/dist/{idle-shutdown.js → daemon/idle-shutdown.js} +27 -4
  52. package/dist/domain/domain-model.js +2 -2
  53. package/dist/events/body-failures.js +8 -8
  54. package/dist/events/contradictions.d.ts +1 -1
  55. package/dist/events/contradictions.js +60 -10
  56. package/dist/events/echo-mismatch.js +10 -10
  57. package/dist/events/event-filters.js +6 -6
  58. package/dist/events/json-salvage.d.ts +2 -1
  59. package/dist/events/json-salvage.js +6 -6
  60. package/dist/events/observed-in-window.d.ts +12 -0
  61. package/dist/events/observed-in-window.js +21 -0
  62. package/dist/events/predicate-eval.d.ts +43 -20
  63. package/dist/events/predicate-eval.js +159 -55
  64. package/dist/events/predicate.d.ts +2 -2
  65. package/dist/events/predicate.js +112 -35
  66. package/dist/events/reconcile.js +5 -5
  67. package/dist/events/ring-buffer.js +1 -1
  68. package/dist/events/stale-response.js +6 -6
  69. package/dist/events/testid-near-miss.d.ts +22 -0
  70. package/dist/events/testid-near-miss.js +33 -0
  71. package/dist/events/unit-mismatch.js +8 -8
  72. package/dist/flows/affected-tools.js +19 -4
  73. package/dist/flows/affected.js +1 -1
  74. package/dist/flows/annotate-target.d.ts +1 -0
  75. package/dist/flows/annotate-target.js +20 -0
  76. package/dist/flows/annotate-tools.js +25 -3
  77. package/dist/flows/annotate.js +17 -4
  78. package/dist/flows/assertion-integrity.d.ts +2 -1
  79. package/dist/flows/assertion-tiers-store.d.ts +3 -66
  80. package/dist/flows/assertion-tiers-store.js +2 -2
  81. package/dist/flows/attributed-failure.d.ts +17 -0
  82. package/dist/flows/attributed-failure.js +20 -0
  83. package/dist/flows/buddy-status.d.ts +2 -1
  84. package/dist/flows/buddy-status.js +2 -2
  85. package/dist/flows/coverage.d.ts +3 -2
  86. package/dist/flows/coverage.js +2 -2
  87. package/dist/flows/decision.js +24 -9
  88. package/dist/flows/empty-flow.d.ts +6 -0
  89. package/dist/flows/empty-flow.js +24 -0
  90. package/dist/flows/flake-store.d.ts +0 -1
  91. package/dist/flows/flake-store.js +1 -1
  92. package/dist/flows/flake.d.ts +2 -1
  93. package/dist/flows/flake.js +2 -2
  94. package/dist/flows/flow-classify.js +12 -0
  95. package/dist/flows/flow-replay-run.js +4 -4
  96. package/dist/flows/flow-replay.d.ts +5 -7
  97. package/dist/flows/flow-replay.js +114 -47
  98. package/dist/flows/flow-report.js +1 -1
  99. package/dist/flows/flow-scope.d.ts +1 -6
  100. package/dist/flows/flow-step-runners.d.ts +26 -6
  101. package/dist/flows/flow-step-runners.js +157 -6
  102. package/dist/flows/flow-success.js +20 -15
  103. package/dist/flows/flow-tools.js +53 -7
  104. package/dist/flows/flows.js +15 -11
  105. package/dist/flows/gate.d.ts +4 -3
  106. package/dist/flows/gate.js +1 -1
  107. package/dist/flows/heal-run.d.ts +0 -8
  108. package/dist/flows/heal-run.js +5 -5
  109. package/dist/flows/heal.js +3 -3
  110. package/dist/flows/predicate-to-expect.d.ts +38 -0
  111. package/dist/flows/predicate-to-expect.js +95 -0
  112. package/dist/flows/replay.d.ts +16 -1
  113. package/dist/flows/replay.js +58 -22
  114. package/dist/flows/role-anchor-nearest.d.ts +6 -0
  115. package/dist/flows/role-anchor-nearest.js +45 -0
  116. package/dist/flows/server-verify.d.ts +1 -1
  117. package/dist/flows/server-verify.js +5 -5
  118. package/dist/flows/verify-change-tools.js +32 -10
  119. package/dist/flows/verify-change-verdict.d.ts +15 -0
  120. package/dist/flows/verify-change-verdict.js +25 -0
  121. package/dist/flows/watch-batcher.d.ts +3 -2
  122. package/dist/honesty/already-true.d.ts +2 -0
  123. package/dist/honesty/already-true.js +31 -0
  124. package/dist/honesty/blind-spots.d.ts +16 -3
  125. package/dist/honesty/blind-spots.js +24 -12
  126. package/dist/honesty/honesty.d.ts +3 -2
  127. package/dist/honesty/honesty.js +4 -4
  128. package/dist/honesty/uncaptured-bodies.js +1 -1
  129. package/dist/honesty/unread-outcome.js +2 -2
  130. package/dist/honesty/verified.d.ts +22 -3
  131. package/dist/honesty/verified.js +48 -8
  132. package/dist/http-server.js +14 -7
  133. package/dist/hunt/hunt-report.d.ts +2 -1
  134. package/dist/hunt/hunt-report.js +2 -2
  135. package/dist/index.d.ts +8 -6
  136. package/dist/index.js +32 -25
  137. package/dist/init/agent-rules.d.ts +2 -1
  138. package/dist/init/agent-rules.js +45 -7
  139. package/dist/init/app-choice.d.ts +20 -0
  140. package/dist/init/app-choice.js +25 -0
  141. package/dist/init/astro-host.d.ts +21 -0
  142. package/dist/init/astro-host.js +27 -0
  143. package/dist/init/astro-patch.d.ts +16 -0
  144. package/dist/init/astro-patch.js +240 -0
  145. package/dist/init/capabilities.js +4 -1
  146. package/dist/init/cra.d.ts +37 -0
  147. package/dist/init/cra.js +90 -0
  148. package/dist/init/cursor.js +39 -4
  149. package/dist/init/desktop-doctor.d.ts +3 -2
  150. package/dist/init/desktop-doctor.js +4 -4
  151. package/dist/init/detect.d.ts +3 -2
  152. package/dist/init/detect.js +16 -5
  153. package/dist/init/mcp-registered.d.ts +12 -0
  154. package/dist/init/mcp-registered.js +14 -0
  155. package/dist/init/mcp.d.ts +27 -2
  156. package/dist/init/mcp.js +40 -4
  157. package/dist/init/next-patch.d.ts +2 -4
  158. package/dist/init/next-patch.js +109 -19
  159. package/dist/init/node-io.js +23 -6
  160. package/dist/init/plan-framework.d.ts +16 -1
  161. package/dist/init/plan-framework.js +132 -8
  162. package/dist/init/plan.d.ts +73 -8
  163. package/dist/init/plan.js +133 -34
  164. package/dist/init/project-id.d.ts +5 -9
  165. package/dist/init/project-id.js +10 -22
  166. package/dist/init/run.d.ts +5 -0
  167. package/dist/init/run.js +210 -36
  168. package/dist/init/slash-command.d.ts +9 -0
  169. package/dist/init/slash-command.js +10 -1
  170. package/dist/init/snippets.d.ts +7 -10
  171. package/dist/init/snippets.js +44 -15
  172. package/dist/init/vite-config.js +1 -1
  173. package/dist/init/workspace-apps.d.ts +23 -0
  174. package/dist/init/workspace-apps.js +68 -0
  175. package/dist/input/driven-redaction.js +1 -1
  176. package/dist/input/network-detail.js +8 -8
  177. package/dist/input/network-mock-tools.js +2 -2
  178. package/dist/input/network-mock.js +4 -4
  179. package/dist/input/real-input.js +8 -3
  180. package/dist/input/scroll-find.js +2 -2
  181. package/dist/input/scroll-tools.d.ts +0 -5
  182. package/dist/input/scroll-tools.js +11 -4
  183. package/dist/input/viewport-tools.js +2 -2
  184. package/dist/journal/ambient-store.d.ts +0 -2
  185. package/dist/journal/ambient-store.js +1 -1
  186. package/dist/journal/ambient.js +1 -1
  187. package/dist/journal/attach-journal.d.ts +2 -1
  188. package/dist/journal/deviation-report.js +6 -6
  189. package/dist/journal/deviation-service.js +1 -1
  190. package/dist/journal/envelope-store.d.ts +0 -2
  191. package/dist/journal/envelope-store.js +1 -1
  192. package/dist/journal/envelope.d.ts +2 -3
  193. package/dist/journal/envelope.js +3 -3
  194. package/dist/journal/journal-query.js +1 -1
  195. package/dist/journal/journal-recorder.d.ts +2 -1
  196. package/dist/journal/journal-recorder.js +2 -2
  197. package/dist/journal/rollups.js +9 -9
  198. package/dist/journal/session-end.d.ts +2 -1
  199. package/dist/journal/session-journal.js +4 -4
  200. package/dist/license/license.d.ts +0 -14
  201. package/dist/license/license.js +8 -8
  202. package/dist/log.d.ts +10 -0
  203. package/dist/log.js +11 -1
  204. package/dist/mcp/mcp-is-error.d.ts +13 -0
  205. package/dist/mcp/mcp-is-error.js +18 -0
  206. package/dist/mcp/mcp-outage.d.ts +31 -0
  207. package/dist/mcp/mcp-outage.js +48 -0
  208. package/dist/mcp/mcp-proxy.d.ts +124 -0
  209. package/dist/mcp/mcp-proxy.js +697 -0
  210. package/dist/{mcp.d.ts → mcp/mcp.d.ts} +13 -10
  211. package/dist/{mcp.js → mcp/mcp.js} +110 -35
  212. package/dist/mcp/proxy-handshake.d.ts +3 -0
  213. package/dist/mcp/proxy-handshake.js +54 -0
  214. package/dist/mcp/proxy-lifecycle.d.ts +64 -0
  215. package/dist/mcp/proxy-lifecycle.js +70 -0
  216. package/dist/mcp/tool-catalog-cache.d.ts +29 -0
  217. package/dist/mcp/tool-catalog-cache.js +55 -0
  218. package/dist/oracles/propose-consequences.d.ts +2 -1
  219. package/dist/oracles/propose-consequences.js +16 -11
  220. package/dist/oracles/self-instrument.d.ts +2 -1
  221. package/dist/oracles/self-instrument.js +1 -1
  222. package/dist/phenomena/phenomena.js +3 -3
  223. package/dist/platform.d.ts +16 -0
  224. package/dist/platform.js +16 -0
  225. package/dist/pool/playwright-launcher.d.ts +15 -2
  226. package/dist/pool/playwright-launcher.js +21 -2
  227. package/dist/project/fs-port.js +1 -1
  228. package/dist/project/project-tools.js +2 -2
  229. package/dist/runs/build-verification-run.js +1 -1
  230. package/dist/runs/repair-prompt.d.ts +0 -6
  231. package/dist/runs/repair-prompt.js +3 -3
  232. package/dist/runs/risk-classify.js +1 -1
  233. package/dist/runs/run-diff.d.ts +0 -2
  234. package/dist/runs/run-diff.js +3 -3
  235. package/dist/runs/run-tools.js +2 -2
  236. package/dist/runs/verification-sync.js +2 -2
  237. package/dist/runs/verify-http.js +1 -1
  238. package/dist/runs/verify-server.js +1 -1
  239. package/dist/session/command-timeout.d.ts +11 -3
  240. package/dist/session/command-timeout.js +2 -2
  241. package/dist/session/control-envelope.d.ts +6 -0
  242. package/dist/session/control-envelope.js +7 -1
  243. package/dist/session/dev-server-probe.d.ts +13 -0
  244. package/dist/session/dev-server-probe.js +52 -0
  245. package/dist/session/last-act.d.ts +17 -3
  246. package/dist/session/last-act.js +16 -4
  247. package/dist/session/live-control-tools.js +7 -2
  248. package/dist/session/live-control.js +1 -1
  249. package/dist/session/looks-like-dev-server.d.ts +14 -0
  250. package/dist/session/looks-like-dev-server.js +25 -0
  251. package/dist/session/no-session-diagnosis.d.ts +38 -0
  252. package/dist/session/no-session-diagnosis.js +70 -0
  253. package/dist/session/no-session-watch.d.ts +19 -0
  254. package/dist/session/no-session-watch.js +61 -0
  255. package/dist/session/observed-state.d.ts +4 -0
  256. package/dist/session/observed-state.js +19 -1
  257. package/dist/session/output-budget.js +1 -1
  258. package/dist/session/session-health.d.ts +8 -1
  259. package/dist/session/session-health.js +8 -7
  260. package/dist/session/session-info.d.ts +26 -7
  261. package/dist/session/session-info.js +42 -1
  262. package/dist/session/session-lease.d.ts +0 -11
  263. package/dist/session/session-lease.js +1 -1
  264. package/dist/session/session-manager.d.ts +3 -0
  265. package/dist/session/session-manager.js +52 -9
  266. package/dist/session/session-readiness.d.ts +1 -1
  267. package/dist/session/session-readiness.js +6 -1
  268. package/dist/session/session-reaper.js +1 -1
  269. package/dist/session/session-reconnect.d.ts +32 -0
  270. package/dist/session/session-reconnect.js +42 -0
  271. package/dist/session/session.d.ts +14 -2
  272. package/dist/session/session.js +69 -26
  273. package/dist/session/state-select.d.ts +1 -1
  274. package/dist/session/state-select.js +1 -1
  275. package/dist/status-payload.d.ts +19 -0
  276. package/dist/status-payload.js +11 -0
  277. package/dist/telemetry/argument-shape.js +10 -1
  278. package/dist/telemetry/browser-mode.d.ts +27 -0
  279. package/dist/telemetry/browser-mode.js +33 -0
  280. package/dist/telemetry/bug-found.d.ts +17 -21
  281. package/dist/telemetry/bug-found.js +120 -8
  282. package/dist/telemetry/cli-telemetry.d.ts +2 -12
  283. package/dist/telemetry/cli-telemetry.js +27 -4
  284. package/dist/telemetry/daemon-telemetry.d.ts +19 -0
  285. package/dist/telemetry/daemon-telemetry.js +12 -2
  286. package/dist/telemetry/dev-repo.js +4 -4
  287. package/dist/telemetry/error-fingerprint.js +21 -2
  288. package/dist/telemetry/feedback-cli.d.ts +1 -1
  289. package/dist/telemetry/feedback-cli.js +4 -4
  290. package/dist/telemetry/feedback-context.d.ts +22 -0
  291. package/dist/telemetry/feedback-context.js +41 -1
  292. package/dist/telemetry/feedback-delivery.d.ts +20 -0
  293. package/dist/telemetry/feedback-delivery.js +30 -0
  294. package/dist/telemetry/feedback-outbox.d.ts +22 -0
  295. package/dist/telemetry/feedback-outbox.js +87 -0
  296. package/dist/telemetry/feedback.d.ts +22 -1
  297. package/dist/telemetry/feedback.js +67 -8
  298. package/dist/telemetry/git-facts.js +1 -1
  299. package/dist/telemetry/session-metrics.d.ts +2 -0
  300. package/dist/telemetry/session-metrics.js +68 -6
  301. package/dist/telemetry/telemetry.d.ts +27 -2
  302. package/dist/telemetry/telemetry.js +119 -29
  303. package/dist/telemetry/verification-of.d.ts +22 -0
  304. package/dist/telemetry/verification-of.js +84 -0
  305. package/dist/tools/act-capsule.js +2 -2
  306. package/dist/tools/act-danger.d.ts +12 -0
  307. package/dist/tools/act-danger.js +20 -3
  308. package/dist/tools/act-helpers.js +4 -4
  309. package/dist/tools/act-tools.d.ts +0 -8
  310. package/dist/tools/act-tools.js +84 -115
  311. package/dist/tools/act-view.js +7 -7
  312. package/dist/tools/alias-args.d.ts +17 -0
  313. package/dist/tools/alias-args.js +25 -0
  314. package/dist/tools/assert-grade.js +9 -9
  315. package/dist/tools/assert-verdict.d.ts +3 -1
  316. package/dist/tools/assert-verdict.js +8 -5
  317. package/dist/tools/browser-tools.js +30 -11
  318. package/dist/tools/contract-tools.js +1 -1
  319. package/dist/tools/coverage-identity.d.ts +34 -0
  320. package/dist/tools/coverage-identity.js +46 -0
  321. package/dist/tools/coverage-tools.js +10 -14
  322. package/dist/tools/dynamic-tools.d.ts +2 -1
  323. package/dist/tools/dynamic-tools.js +90 -18
  324. package/dist/tools/error-recovery.d.ts +20 -2
  325. package/dist/tools/error-recovery.js +179 -4
  326. package/dist/tools/feedback-tools.d.ts +12 -1
  327. package/dist/tools/feedback-tools.js +28 -5
  328. package/dist/tools/invoke-tool.d.ts +0 -6
  329. package/dist/tools/invoke-tool.js +57 -32
  330. package/dist/tools/lease-hint.d.ts +17 -0
  331. package/dist/tools/lease-hint.js +24 -0
  332. package/dist/tools/lease-tools.js +6 -5
  333. package/dist/tools/merge-tools.d.ts +10 -1
  334. package/dist/tools/merge-tools.js +9 -3
  335. package/dist/tools/merged-name-redirect.d.ts +13 -0
  336. package/dist/tools/merged-name-redirect.js +55 -0
  337. package/dist/tools/navigate-result.d.ts +17 -0
  338. package/dist/tools/navigate-result.js +31 -0
  339. package/dist/tools/observe-tools.js +54 -20
  340. package/dist/tools/observed-nothing.d.ts +25 -0
  341. package/dist/tools/observed-nothing.js +33 -0
  342. package/dist/tools/query-paginate.js +2 -2
  343. package/dist/tools/query-shape.d.ts +18 -0
  344. package/dist/tools/query-shape.js +39 -0
  345. package/dist/tools/read-tools.js +81 -20
  346. package/dist/tools/real-input-attempt.d.ts +28 -0
  347. package/dist/tools/real-input-attempt.js +88 -0
  348. package/dist/tools/reconcile-tools.js +7 -7
  349. package/dist/tools/reload-result.d.ts +14 -0
  350. package/dist/tools/reload-result.js +29 -0
  351. package/dist/tools/snapshot-delta.d.ts +4 -5
  352. package/dist/tools/snapshot-delta.js +0 -0
  353. package/dist/tools/tool-kit.d.ts +8 -0
  354. package/dist/tools/tool-surface.d.ts +83 -0
  355. package/dist/tools/tool-surface.js +204 -0
  356. package/dist/tools/tools-helpers.js +6 -6
  357. package/dist/tools/tools.d.ts +15 -0
  358. package/dist/tools/tools.js +53 -17
  359. package/dist/tools/unadvertised-help.d.ts +5 -0
  360. package/dist/tools/unadvertised-help.js +39 -0
  361. package/dist/trace.d.ts +69 -0
  362. package/dist/trace.js +150 -0
  363. package/dist/update/nudge-credit.d.ts +4 -0
  364. package/dist/update/nudge-credit.js +53 -0
  365. package/dist/update/sdk-sync.d.ts +21 -0
  366. package/dist/update/sdk-sync.js +42 -0
  367. package/dist/update/update-checker.js +3 -3
  368. package/dist/update/update-nudge.d.ts +63 -6
  369. package/dist/update/update-nudge.js +128 -15
  370. package/dist/update/updater.d.ts +1 -9
  371. package/dist/update/updater.js +13 -8
  372. package/dist/version/peer-announce.d.ts +4 -0
  373. package/dist/version/peer-announce.js +30 -0
  374. package/dist/{server-version.js → version/server-version.js} +5 -1
  375. package/dist/version/version-nudge.d.ts +28 -0
  376. package/dist/version/version-nudge.js +40 -0
  377. package/dist/version/version-skew.d.ts +66 -0
  378. package/dist/version/version-skew.js +71 -0
  379. package/dist/visual/visual-diff.js +2 -2
  380. package/dist/visual/visual-tools.js +4 -4
  381. package/docs/agent-cheatsheet.md +85 -0
  382. package/docs/architecture.md +113 -0
  383. package/docs/benchmarks.md +255 -0
  384. package/docs/debugging.md +102 -0
  385. package/docs/deploy-checks.md +69 -0
  386. package/docs/desktop-apps.md +256 -0
  387. package/docs/enterprise.md +62 -0
  388. package/docs/fixtures.md +28 -0
  389. package/docs/flows.md +166 -0
  390. package/docs/getting-started.md +469 -0
  391. package/docs/human-control.md +63 -0
  392. package/docs/integration-patterns.md +171 -0
  393. package/docs/local-registry.md +126 -0
  394. package/docs/mint.json +61 -0
  395. package/docs/multi-agent-testing.md +55 -0
  396. package/docs/platform-integration.md +122 -0
  397. package/docs/telemetry-contract.md +214 -0
  398. package/docs/telemetry.md +184 -0
  399. package/docs/testing.md +82 -0
  400. package/docs/token-efficiency.md +80 -0
  401. package/docs/usage.md +1046 -0
  402. package/package.json +8 -5
  403. package/dist/cli-launch.js +0 -112
  404. package/dist/daemon-resilience.d.ts +0 -22
  405. package/dist/daemon-resilience.js +0 -85
  406. package/dist/mcp-proxy.d.ts +0 -67
  407. package/dist/mcp-proxy.js +0 -337
  408. package/dist/tools/profiles.d.ts +0 -37
  409. package/dist/tools/profiles.js +0 -139
  410. /package/dist/{pairing-token.d.ts → bridge/pairing-token.d.ts} +0 -0
  411. /package/dist/{token-auth.d.ts → bridge/token-auth.d.ts} +0 -0
  412. /package/dist/{token-auth.js → bridge/token-auth.js} +0 -0
  413. /package/dist/{cli-doctor.d.ts → cli/cli-doctor.d.ts} +0 -0
  414. /package/dist/{cli-verify.d.ts → cli/cli-verify.d.ts} +0 -0
  415. /package/dist/{cloud-cli.d.ts → cli/cloud-cli.d.ts} +0 -0
  416. /package/dist/{server-version.d.ts → version/server-version.d.ts} +0 -0
@@ -15,18 +15,18 @@ export const CloudEnv = {
15
15
  KEY: 'RETICLE_CLOUD_KEY',
16
16
  };
17
17
  /** Paths the OSS server pushes to (match the cloud app's contract). */
18
- export const CLOUD_FLOWS_PATH = '/v1/flows';
19
- export const CLOUD_RUNS_PATH = '/v1/runs';
20
- export const CLOUD_PROJECT_RUNS_PATH = '/v1/project/runs';
21
- export const CLOUD_PROJECT_REGRESSION_PATH = '/v1/project/regression';
22
- export const CLOUD_VERIFICATIONS_PATH = '/v1/verifications';
18
+ const CLOUD_FLOWS_PATH = '/v1/flows';
19
+ const CLOUD_RUNS_PATH = '/v1/runs';
20
+ const CLOUD_PROJECT_RUNS_PATH = '/v1/project/runs';
21
+ const CLOUD_PROJECT_REGRESSION_PATH = '/v1/project/regression';
22
+ const CLOUD_VERIFICATIONS_PATH = '/v1/verifications';
23
23
  /** Resolve cloud credentials from the environment, or null when not logged in (sync disabled). */
24
24
  export function resolveCloudConfig(env) {
25
25
  const url = env[CloudEnv.URL];
26
26
  const apiKey = env[CloudEnv.KEY];
27
- if (typeof url !== 'string' || url.length === 0)
27
+ if (typeof url !== 'string' || 0 === url.length)
28
28
  return null;
29
- if (typeof apiKey !== 'string' || apiKey.length === 0)
29
+ if (typeof apiKey !== 'string' || 0 === apiKey.length)
30
30
  return null;
31
31
  return { url: url.replace(/\/+$/, ''), apiKey };
32
32
  }
@@ -40,7 +40,7 @@ export const SyncOutcome = {
40
40
  * throws — a failure is reported, not propagated, so the local save is authoritative.
41
41
  */
42
42
  export async function syncFlowToCloud(flow, config, projectId, fetchImpl) {
43
- if (config === null)
43
+ if (null === config)
44
44
  return { outcome: SyncOutcome.SKIPPED };
45
45
  try {
46
46
  const res = await fetchImpl(`${config.url}${CLOUD_FLOWS_PATH}`, {
@@ -67,7 +67,7 @@ export async function syncFlowToCloud(flow, config, projectId, fetchImpl) {
67
67
  * built it), so the body is the run itself, not a wrapper. Best-effort: never throws, never blocks exit.
68
68
  */
69
69
  export async function syncRunToCloud(run, config, fetchImpl) {
70
- if (config === null)
70
+ if (null === config)
71
71
  return { outcome: SyncOutcome.SKIPPED };
72
72
  try {
73
73
  const res = await fetchImpl(`${config.url}${CLOUD_RUNS_PATH}`, {
@@ -93,7 +93,7 @@ export async function syncRunToCloud(run, config, fetchImpl) {
93
93
  * truth). `projectId` scopes an org's multiple suites. Best-effort: never throws, never blocks the tool.
94
94
  */
95
95
  export async function syncRunRecordToCloud(record, projectId, config, fetchImpl) {
96
- if (config === null)
96
+ if (null === config)
97
97
  return { outcome: SyncOutcome.SKIPPED };
98
98
  try {
99
99
  const res = await fetchImpl(`${config.url}${CLOUD_PROJECT_RUNS_PATH}`, {
@@ -127,7 +127,7 @@ export async function syncRunRecordToCloud(record, projectId, config, fetchImpl)
127
127
  * cloud), or null when not logged in / unreachable — so the caller degrades to local-only, never fails.
128
128
  */
129
129
  export async function fetchProjectRegressionFromCloud(config, projectId, fetchImpl) {
130
- if (config === null)
130
+ if (null === config)
131
131
  return null;
132
132
  try {
133
133
  const query = projectId === undefined ? '' : `?projectId=${encodeURIComponent(projectId)}`;
@@ -156,7 +156,7 @@ const ServerVerificationSchema = z.object({
156
156
  * submit fails, so the caller falls back to a local replay. Never throws.
157
157
  */
158
158
  export async function submitServerVerification(body, config, fetchImpl) {
159
- if (config === null)
159
+ if (null === config)
160
160
  return null;
161
161
  try {
162
162
  const res = await fetchImpl(`${config.url}${CLOUD_VERIFICATIONS_PATH}`, {
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Why a crawl clicked nothing.
3
+ *
4
+ * `stepsRun: 0` came back with no `error` and nothing else, so "the page had no controls" and "it had
5
+ * 34 and none were clicked" were the same answer. Reported from a sweep on a page with 34 of them,
6
+ * and not reproducible here — crawl drives the bench app correctly — so the honest move is to make
7
+ * the zero carry its own diagnosis rather than guess at a cause.
8
+ */
9
+ interface CrawlOutcome {
10
+ interactiveFound: number;
11
+ stepsRun: number;
12
+ maxSteps: number;
13
+ truncated: boolean;
14
+ }
15
+ export declare function crawlEmptyNote(outcome: CrawlOutcome): string | undefined;
16
+ export {};
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Why a crawl clicked nothing.
3
+ *
4
+ * `stepsRun: 0` came back with no `error` and nothing else, so "the page had no controls" and "it had
5
+ * 34 and none were clicked" were the same answer. Reported from a sweep on a page with 34 of them,
6
+ * and not reproducible here — crawl drives the bench app correctly — so the honest move is to make
7
+ * the zero carry its own diagnosis rather than guess at a cause.
8
+ */
9
+ export function crawlEmptyNote(outcome) {
10
+ if (outcome.stepsRun > 0)
11
+ return undefined;
12
+ if (outcome.maxSteps <= 0) {
13
+ return 'nothing was clicked because maxSteps is 0 — raise it to crawl at all';
14
+ }
15
+ if (0 === outcome.interactiveFound) {
16
+ const capped = outcome.truncated
17
+ ? ' The snapshot was also truncated, so controls past the cap were never seen — narrow `scope` and retry.'
18
+ : '';
19
+ return `no interactive controls were found on this page, so nothing was clicked — the crawl ran and found none, which is a result rather than a failure.${capped}`;
20
+ }
21
+ return (`${String(outcome.interactiveFound)} interactive controls were found and none of them were clicked. ` +
22
+ `That is not an empty page, so something stopped the loop — report it with reticle_feedback, ` +
23
+ `including this page's URL and one control's ref.`);
24
+ }
@@ -37,6 +37,9 @@ export const CRAWL_TOOLS = [
37
37
  outputSchema: {
38
38
  interactiveFound: z.number(),
39
39
  stepsRun: z.number(),
40
+ // Present only when nothing was clicked, saying WHICH zero this is — an empty page, a step
41
+ // budget of zero, or controls found and none driven. A bare zero was unactionable.
42
+ note: z.string().optional(),
40
43
  anomalies: z.array(z.object({
41
44
  kind: z.string(),
42
45
  ref: z.string(),
@@ -64,7 +67,7 @@ export const CRAWL_TOOLS = [
64
67
  ...(maxSteps !== undefined ? { maxSteps } : {}),
65
68
  ...(settleMs !== undefined ? { settleMs } : {}),
66
69
  ...(scope !== undefined ? { scope } : {}),
67
- ...(args['confirmDangerous'] === true ? { confirmDangerous: true } : {}),
70
+ ...(true === args['confirmDangerous'] ? { confirmDangerous: true } : {}),
68
71
  };
69
72
  return crawl(session, opts, nodeSleep);
70
73
  },
@@ -1,4 +1,6 @@
1
+ import { span } from '../trace.js';
1
2
  import { ActionType, CRAWL_DEFAULTS, CrawlAnomalyKind, ContradictionKind, DANGEROUS_ACTION_CONFIRM_ARG, EventType, ReticleCommand, } from '@reticlehq/core';
3
+ import { crawlEmptyNote } from './crawl-empty.js';
2
4
  import { parseInteractive, asRecord, asNumber, asString, sourceOf, } from '../tools/tools-helpers.js';
3
5
  import { ReticleTool } from '../tools/tool-names.js';
4
6
  import { findContradictions } from '../events/contradictions.js';
@@ -62,7 +64,7 @@ export async function crawl(session, opts, sleep) {
62
64
  // the controls it never reached are not merely unclicked — they were never seen. Reporting
63
65
  // `interactiveFound` without this would state a control count that is really a cap, and
64
66
  // `truncated:false` would positively assert that nothing was cut.
65
- const coverageCapped = snapshot.truncated === true;
67
+ const coverageCapped = true === snapshot.truncated;
66
68
  const anomalies = [];
67
69
  const visited = [];
68
70
  const counts = { consoleErrors: 0, failedRequests: 0, deadControls: 0, contradictions: 0 };
@@ -83,12 +85,18 @@ export async function crawl(session, opts, sleep) {
83
85
  session.beginAction?.(ReticleTool.CRAWL, { ref: item.ref, action: ActionType.CLICK });
84
86
  let act;
85
87
  try {
86
- act = await session.command(ReticleCommand.ACT, {
87
- ref: item.ref,
88
- action: ActionType.CLICK,
89
- args: opts.confirmDangerous === true ? { [DANGEROUS_ACTION_CONFIRM_ARG]: true } : {},
88
+ // One span per control clicked, so a slow crawl names the control rather than reporting a
89
+ // single multi-second total. The settle sleep below is INSIDE it deliberately: it is part of
90
+ // what a step costs, and hiding it would make the fixed budget look free.
91
+ act = await span('crawl.step', { ref: item.ref, desc: item.desc }, async () => {
92
+ const clicked = await session.command(ReticleCommand.ACT, {
93
+ ref: item.ref,
94
+ action: ActionType.CLICK,
95
+ args: true === opts.confirmDangerous ? { [DANGEROUS_ACTION_CONFIRM_ARG]: true } : {},
96
+ });
97
+ await sleep(settleMs);
98
+ return clicked;
90
99
  });
91
- await sleep(settleMs);
92
100
  }
93
101
  finally {
94
102
  // Close on every exit so a throw cannot leak the window onto the next control's events.
@@ -147,7 +155,7 @@ export async function crawl(session, opts, sleep) {
147
155
  // noise for the false negative this check exists to catch.
148
156
  const dispatched = asRecord(act.result)['dispatched'] !== false && act.ok;
149
157
  if (dispatched &&
150
- errs.length === 0 &&
158
+ 0 === errs.length &&
151
159
  !events.some(isActivity) &&
152
160
  !legitimatelyInert(item.desc)) {
153
161
  counts.deadControls += 1;
@@ -171,5 +179,16 @@ export async function crawl(session, opts, sleep) {
171
179
  // partial sweep into "all controls healthy".
172
180
  truncated: items.length > stepsRun || coverageCapped,
173
181
  ...(coverageCapped ? { coverageNote: CAPPED_SNAPSHOT_NOTE } : {}),
182
+ // A bare `stepsRun: 0` made "the page had no controls" and "it had 34 and none were clicked" the
183
+ // same answer. See crawl-empty.
184
+ ...(() => {
185
+ const note = crawlEmptyNote({
186
+ interactiveFound: items.length,
187
+ stepsRun,
188
+ maxSteps,
189
+ truncated: coverageCapped,
190
+ });
191
+ return note === undefined ? {} : { note };
192
+ })(),
174
193
  };
175
194
  }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Process-level resilience for the long-running daemon. The daemon serves many agents at once, so a
3
+ * single stray error must not take the whole fleet down:
4
+ *
5
+ * - unhandledRejection (a fire-and-forget promise nobody awaited — the common case in async WS/pool
6
+ * code) → LOG and keep running. One agent's async slip-up can't crash the daemon for everyone.
7
+ * - uncaughtException (a synchronous throw escaped all try/catch) → the process state is undefined
8
+ * per Node's guidance, so LOG a clear reason and exit cleanly; the next `reticle mcp` respawns a fresh
9
+ * daemon, which beats crashing silently or limping along corrupt.
10
+ */
11
+ export interface ProcessLike {
12
+ on(event: string, listener: (arg: unknown) => void): unknown;
13
+ }
14
+ type LogFn = (event: string, data: Record<string, unknown>) => void;
15
+ /** The two ways a failure reaches the top of the process. Named so the analytics can tell them apart. */
16
+ export declare const CrashKind: {
17
+ readonly UNHANDLED_REJECTION: "unhandled_rejection";
18
+ readonly UNCAUGHT_EXCEPTION: "uncaught_exception";
19
+ };
20
+ export type CrashKind = (typeof CrashKind)[keyof typeof CrashKind];
21
+ /**
22
+ * Log EVERY way this process can leave, so a death always leaves a line.
23
+ *
24
+ * Reported from the field: a 115-line daemon log containing zero of `reticle_daemon_idle_exit`,
25
+ * `reticle_daemon_close_error`, `daemon_stopped`, `uncaught` or `unhandled` — the daemon simply
26
+ * stopped existing mid-wait. Both crash handlers below already log before exiting, so their silence
27
+ * ruled themselves out and left nothing else to read: no shutdown path had run at all.
28
+ *
29
+ * `exit` fires for every in-process exit including an explicit `process.exit`, and carries the code.
30
+ * The signals cover an external kill. What neither can catch is SIGKILL or an OOM abort — and that
31
+ * is the point: after this, silence in the log is itself the finding, because every other door is
32
+ * now instrumented.
33
+ */
34
+ export declare function installExitTrace(proc: ProcessLike, log: LogFn): void;
35
+ export declare function installDaemonResilience(proc: ProcessLike, log: LogFn, onFatal: () => void): void;
36
+ /**
37
+ * Process-level resilience for the MCP PROXY, whose rule is the opposite of the daemon's.
38
+ *
39
+ * The daemon exits on an uncaught throw because the next `reticle mcp` respawns it. Nothing respawns
40
+ * the proxy — it is the stdio server the editor launched, and its exit is what the user experiences
41
+ * as "the MCP server disconnected, open /mcp and reconnect". So it logs and keeps serving: its whole
42
+ * state is a socket and a queue, and it already knows how to rebuild both (see the reconnect and
43
+ * dormant paths). A crashed-but-serving proxy answers the handshake and `tools/list` from cache; a
44
+ * dead one answers nothing and needs a human.
45
+ *
46
+ * `onCrash` is for reporting only — it must not end the process.
47
+ */
48
+ export declare function installProxyResilience(proc: ProcessLike, log: LogFn, onCrash?: (kind: CrashKind, cause: unknown) => void): void;
49
+ export {};
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Process-level resilience for the long-running daemon. The daemon serves many agents at once, so a
3
+ * single stray error must not take the whole fleet down:
4
+ *
5
+ * - unhandledRejection (a fire-and-forget promise nobody awaited — the common case in async WS/pool
6
+ * code) → LOG and keep running. One agent's async slip-up can't crash the daemon for everyone.
7
+ * - uncaughtException (a synchronous throw escaped all try/catch) → the process state is undefined
8
+ * per Node's guidance, so LOG a clear reason and exit cleanly; the next `reticle mcp` respawns a fresh
9
+ * daemon, which beats crashing silently or limping along corrupt.
10
+ */
11
+ import { TelemetryActor, TelemetryEventKind } from '@reticlehq/core';
12
+ import { errorSkeleton, errorTypeOf, fingerprintCrash, MAX_REPORTED_FRAMES, reticleFrames, } from '../telemetry/error-fingerprint.js';
13
+ import { getSessionMetrics } from '../telemetry/session-metrics.js';
14
+ import { machineSnapshot } from '../telemetry/machine-snapshot.js';
15
+ import { getTelemetry } from '../telemetry/telemetry.js';
16
+ function describe(value) {
17
+ return value instanceof Error ? value.message : String(value);
18
+ }
19
+ /**
20
+ * Node error codes that mean THE OTHER END WENT AWAY, not "we broke".
21
+ *
22
+ * One real session produced nine `runtime_crashed` events, all `write EPIPE`: the MCP client closed
23
+ * its half of the stdio pipe and the very next `process.stdout.write` failed, which is the ordinary
24
+ * way a client leaves. Counted as uncaught exceptions they poison the only metric that answers "is
25
+ * Reticle stable?" — and they recur every single time an editor is closed.
26
+ *
27
+ * Matched on `err.code`, never on the message: a message is prose, gets wrapped, localised and
28
+ * rewritten, and matching it would eventually swallow a real crash that merely mentioned a pipe.
29
+ *
30
+ * - `EPIPE` — wrote to a pipe whose reader is gone. The stdio case, and the nine.
31
+ * - `ECONNRESET` — the peer reset the socket; stdio is a socket when the parent piped it, and the
32
+ * HTTP/SSE surface produces it when a client vanishes mid-response.
33
+ * - `ERR_STREAM_DESTROYED` — the same race one tick later: the stream was already torn down when
34
+ * the write landed.
35
+ */
36
+ const DISCONNECT_CODES = new Set([
37
+ 'EPIPE',
38
+ 'ECONNRESET',
39
+ 'ERR_STREAM_DESTROYED',
40
+ ]);
41
+ /** The disconnect code this failure carries, if that is all it was. */
42
+ function disconnectCode(value) {
43
+ const code = value?.code;
44
+ return 'string' === typeof code && DISCONNECT_CODES.has(code) ? code : undefined;
45
+ }
46
+ /**
47
+ * Log an expected disconnect and report `true`, so the caller skips the crash report.
48
+ *
49
+ * It is still WRITTEN DOWN. A client leaving is normal, but a daemon that suddenly logs a hundred of
50
+ * these is a finding — the difference between "not a crash" and "not worth knowing" is the whole
51
+ * reason this logs instead of returning early in silence.
52
+ */
53
+ function absorbDisconnect(value, log, event) {
54
+ const code = disconnectCode(value);
55
+ if (code === undefined)
56
+ return false;
57
+ log(event, { code, note: 'the client went away — expected, not a crash' });
58
+ return true;
59
+ }
60
+ /** The two ways a failure reaches the top of the process. Named so the analytics can tell them apart. */
61
+ export const CrashKind = {
62
+ UNHANDLED_REJECTION: 'unhandled_rejection',
63
+ UNCAUGHT_EXCEPTION: 'uncaught_exception',
64
+ };
65
+ /**
66
+ * Report a crash with enough detail to actually diagnose it.
67
+ *
68
+ * This is the only place in the product that learns about a crash at all. An uncaught exception in a
69
+ * long-running daemon takes down every agent attached to it, and the only other record is a line in a
70
+ * local log nobody sends us.
71
+ *
72
+ * The first version sent a hash and nothing else, which made crashes rankable and undiagnosable — you
73
+ * could see that forty machines hit `a3f2c1d8e9b0` and had no way to learn what that was. So it now
74
+ * carries the four things an RCA needs: WHERE (our own frames, with function names and line numbers),
75
+ * WHAT (the message with every variable part stripped), WHEN (the tool in flight), and WHY-ish (the
76
+ * agent's approach run before it). All of it is our code and our vocabulary; the user's stack frames,
77
+ * their paths, and the contents of their message are removed before any of it is built.
78
+ *
79
+ * Best-effort and wrapped: reporting a crash must never be the reason a crash gets worse.
80
+ */
81
+ function reportCrash(kind, value) {
82
+ try {
83
+ const errorType = errorTypeOf(value);
84
+ const stack = value instanceof Error ? (value.stack ?? '') : '';
85
+ const message = describe(value);
86
+ const { breadcrumb, inFlight } = getSessionMetrics().trail;
87
+ const machine = machineSnapshot();
88
+ void getTelemetry().emit(TelemetryEventKind.RUNTIME_CRASHED, {
89
+ // A crash is ALWAYS reached through something the agent asked for — the daemon does nothing on
90
+ // its own — so attributing it to the agent is accurate rather than a guess.
91
+ actor: TelemetryActor.AGENT,
92
+ crash: {
93
+ kind,
94
+ errorType,
95
+ fingerprint: fingerprintCrash(errorType, stack, message),
96
+ // The skeleton, not the message: every quoted string, path, URL, id and number is replaced
97
+ // by `*` first. This is what makes the fingerprint mean something to a human reading a chart.
98
+ message: errorSkeleton(message).slice(0, 300),
99
+ // Our own frames, our own published code. The file, the line, and the function — which is the
100
+ // whole of "where did it break". Frames from the user's app never survive this.
101
+ frames: reticleFrames(stack).slice(0, MAX_REPORTED_FRAMES),
102
+ ...(inFlight !== undefined ? { tool: inFlight } : {}),
103
+ ...(breadcrumb.length > 0 ? { breadcrumb } : {}),
104
+ // Crashes cluster hard by runtime version and architecture, and we were recording neither.
105
+ nodeVersion: process.versions.node,
106
+ arch: process.arch,
107
+ // "Out of memory" and "our bug" look identical in a stack trace. This is what tells them apart.
108
+ ...(machine !== undefined ? { machine } : {}),
109
+ },
110
+ });
111
+ }
112
+ catch {
113
+ /* a crash report must never be the reason a crash gets worse */
114
+ }
115
+ }
116
+ /**
117
+ * Log EVERY way this process can leave, so a death always leaves a line.
118
+ *
119
+ * Reported from the field: a 115-line daemon log containing zero of `reticle_daemon_idle_exit`,
120
+ * `reticle_daemon_close_error`, `daemon_stopped`, `uncaught` or `unhandled` — the daemon simply
121
+ * stopped existing mid-wait. Both crash handlers below already log before exiting, so their silence
122
+ * ruled themselves out and left nothing else to read: no shutdown path had run at all.
123
+ *
124
+ * `exit` fires for every in-process exit including an explicit `process.exit`, and carries the code.
125
+ * The signals cover an external kill. What neither can catch is SIGKILL or an OOM abort — and that
126
+ * is the point: after this, silence in the log is itself the finding, because every other door is
127
+ * now instrumented.
128
+ */
129
+ export function installExitTrace(proc, log) {
130
+ proc.on('exit', (code) => {
131
+ log('reticle_daemon_exiting', { code: 'number' === typeof code ? code : null });
132
+ });
133
+ for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) {
134
+ proc.on(signal, () => {
135
+ // Log only. Whatever installed a real handler for this signal still runs; adding an exit here
136
+ // would race the shutdown that flushes the session summary.
137
+ log('reticle_daemon_signalled', { signal });
138
+ });
139
+ }
140
+ }
141
+ export function installDaemonResilience(proc, log, onFatal) {
142
+ installExitTrace(proc, log);
143
+ const disconnected = 'reticle_daemon_client_disconnected';
144
+ proc.on('unhandledRejection', (reason) => {
145
+ if (absorbDisconnect(reason, log, disconnected))
146
+ return;
147
+ log('reticle_daemon_unhandled_rejection', { reason: describe(reason) });
148
+ reportCrash(CrashKind.UNHANDLED_REJECTION, reason);
149
+ });
150
+ proc.on('uncaughtException', (err) => {
151
+ // Not fatal either: Node's "the process state is undefined" guidance is about a throw that
152
+ // escaped everything, not about a write to a socket somebody closed. Exiting here would let one
153
+ // departing client take down the daemon serving every other agent.
154
+ if (absorbDisconnect(err, log, disconnected))
155
+ return;
156
+ log('reticle_daemon_uncaught_exception', { error: describe(err) });
157
+ reportCrash(CrashKind.UNCAUGHT_EXCEPTION, err);
158
+ onFatal();
159
+ });
160
+ }
161
+ /**
162
+ * Process-level resilience for the MCP PROXY, whose rule is the opposite of the daemon's.
163
+ *
164
+ * The daemon exits on an uncaught throw because the next `reticle mcp` respawns it. Nothing respawns
165
+ * the proxy — it is the stdio server the editor launched, and its exit is what the user experiences
166
+ * as "the MCP server disconnected, open /mcp and reconnect". So it logs and keeps serving: its whole
167
+ * state is a socket and a queue, and it already knows how to rebuild both (see the reconnect and
168
+ * dormant paths). A crashed-but-serving proxy answers the handshake and `tools/list` from cache; a
169
+ * dead one answers nothing and needs a human.
170
+ *
171
+ * `onCrash` is for reporting only — it must not end the process.
172
+ */
173
+ export function installProxyResilience(proc, log, onCrash = reportCrash) {
174
+ const disconnected = 'reticle_mcp_proxy_client_disconnected';
175
+ proc.on('unhandledRejection', (reason) => {
176
+ if (absorbDisconnect(reason, log, disconnected))
177
+ return;
178
+ log('reticle_mcp_proxy_unhandled_rejection', { reason: describe(reason) });
179
+ onCrash(CrashKind.UNHANDLED_REJECTION, reason);
180
+ });
181
+ proc.on('uncaughtException', (err) => {
182
+ // The client closing its end of stdio is how this process is SUPPOSED to end its day.
183
+ if (absorbDisconnect(err, log, disconnected))
184
+ return;
185
+ log('reticle_mcp_proxy_uncaught_exception', {
186
+ error: describe(err),
187
+ note: 'still serving — exiting here would disconnect the MCP client',
188
+ });
189
+ onCrash(CrashKind.UNCAUGHT_EXCEPTION, err);
190
+ });
191
+ }
@@ -0,0 +1,61 @@
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
+ export declare function noteToolCall(): void;
24
+ export declare function everServedToolCall(): boolean;
25
+ /** Tests only. */
26
+ export declare function resetDaemonUsefulness(): void;
27
+ interface UsefulnessFacts {
28
+ /** Has any tool been dispatched since this daemon booted? */
29
+ servedToolCall: boolean;
30
+ /** Has any browser session ever connected? */
31
+ everConnected: boolean;
32
+ /** Active pool leases — a lease means somebody is mid-flight even with no session yet. */
33
+ activeLeases: number;
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 declare function isUselessDaemon(facts: UsefulnessFacts): boolean;
42
+ /** The little of SessionManager the idle predicate reads — structural, so a fake needs two methods. */
43
+ interface IdleSessions {
44
+ count(): number;
45
+ everConnected(): boolean;
46
+ }
47
+ /** The little of BrowserPool it reads. */
48
+ interface IdlePool {
49
+ activeCount(): number;
50
+ }
51
+ /**
52
+ * The daemon's whole "is anything happening" question.
53
+ *
54
+ * Two ways to be idle. The original — nobody attached, nothing connected. And the one that was
55
+ * missing: an agent IS attached but this daemon has never served a tool call and no browser has ever
56
+ * connected to it. `agentConnected` stays true for an entire editor session, so without the second
57
+ * clause the shutdown watcher could never fire on the 74% of daemons that spawn in a directory with
58
+ * no app and sit there for half an hour.
59
+ */
60
+ export declare function buildIdlePredicate(agentConnected: () => boolean, sessions: IdleSessions, pool: IdlePool): () => boolean;
61
+ export {};
@@ -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