@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
@@ -24,8 +24,11 @@
24
24
  import { FeedbackSchema, FEEDBACK_TEXT_MAX, FEEDBACK_TRACE_MAX, FEEDBACK_FIELD_MAX, TelemetryEventKind, } from '@reticlehq/core';
25
25
  import { platform } from 'node:os';
26
26
  import { getTelemetry } from './telemetry.js';
27
- import { SERVER_VERSION } from '../server-version.js';
27
+ import { noteFeedbackUndelivered } from './feedback-delivery.js';
28
+ import { isReticleSourceCheckout } from './dev-repo.js';
29
+ import { SERVER_VERSION } from '../version/server-version.js';
28
30
  import { feedbackContext } from './feedback-context.js';
31
+ import { markDelivered, outboxPath, queueFeedback } from './feedback-outbox.js';
29
32
  /** Kills the feedback channel ONLY — adoption counters keep flowing. */
30
33
  export const FEEDBACK_ENV = 'RETICLE_FEEDBACK';
31
34
  /** What replaces a redacted match. Visible on purpose: a reader must see that something was removed. */
@@ -67,7 +70,7 @@ export function redactFeedbackText(input, max) {
67
70
  removed.add(label);
68
71
  // A rule with a leading capture keeps it (the scheme, the `api_key=` key) so the reader still
69
72
  // sees WHAT was removed, not just that something was.
70
- const prefix = typeof groups[0] === 'string' ? groups[0] : '';
73
+ const prefix = 'string' === typeof groups[0] ? groups[0] : '';
71
74
  return `${prefix}${REDACTED}`;
72
75
  });
73
76
  }
@@ -76,7 +79,7 @@ export function redactFeedbackText(input, max) {
76
79
  /** True when the feedback channel is switched off on its own (the emitter's switches apply as well). */
77
80
  export function feedbackDisabled(env = process.env) {
78
81
  const value = (env[FEEDBACK_ENV] ?? '').toLowerCase();
79
- return value === '0' || value === 'false' || value === 'off' || value === 'no';
82
+ return '0' === value || 'false' === value || 'off' === value || 'no' === value;
80
83
  }
81
84
  /**
82
85
  * The fields an author writes, and therefore the fields that must be redacted. ONE list, used both to
@@ -118,7 +121,7 @@ export async function submitFeedback(input, opts = {}) {
118
121
  // addition. Deriving the set means a sixth field is covered the moment it exists.
119
122
  const redactions = new Map();
120
123
  for (const [key, value] of Object.entries(input)) {
121
- if (typeof value !== 'string' || value === '')
124
+ if (typeof value !== 'string' || '' === value)
122
125
  continue;
123
126
  if (!AUTHOR_WRITTEN_FIELDS.has(key))
124
127
  continue;
@@ -127,13 +130,25 @@ export async function submitFeedback(input, opts = {}) {
127
130
  const body = redactions.get('text') ?? { text: '', removed: [] };
128
131
  const redacted = [...new Set([...redactions.values()].flatMap((r) => r.removed))];
129
132
  if (feedbackDisabled(env)) {
130
- return { sent: false, reason: `feedback is disabled by ${FEEDBACK_ENV}`, redacted, context };
133
+ return {
134
+ sent: false,
135
+ accepted: false,
136
+ reason: `feedback is disabled by ${FEEDBACK_ENV}`,
137
+ redacted,
138
+ context,
139
+ };
131
140
  }
132
141
  const telemetry = getTelemetry();
133
142
  if (!telemetry.enabled) {
134
143
  return {
135
144
  sent: false,
136
- reason: 'telemetry is disabled on this machine, so feedback has nowhere to go',
145
+ accepted: false,
146
+ // Named precisely, because the commonest cause is not what the old wording suggested: a
147
+ // Reticle SOURCE CHECKOUT disables telemetry by cwd, and that is exactly where release runs and
148
+ // contributor sessions happen — so the reports most worth having were the ones silently lost.
149
+ reason: isReticleSourceCheckout(opts.cwd ?? process.cwd())
150
+ ? 'this is a Reticle source checkout, where telemetry is disabled by design — so feedback has nowhere to go. Open an issue at https://github.com/reticlehq/reticle/issues instead, or run from the app you are verifying.'
151
+ : `telemetry is disabled on this machine, so feedback has nowhere to go. Re-enable with \`reticle telemetry enable\`, or open an issue at https://github.com/reticlehq/reticle/issues.`,
137
152
  redacted,
138
153
  context,
139
154
  };
@@ -151,13 +166,57 @@ export async function submitFeedback(input, opts = {}) {
151
166
  if (!parsed.success) {
152
167
  return {
153
168
  sent: false,
169
+ accepted: false,
154
170
  reason: parsed.error.issues[0]?.message ?? 'invalid feedback',
155
171
  redacted,
156
172
  context,
157
173
  };
158
174
  }
159
- await telemetry.emit(TelemetryEventKind.FEEDBACK_SUBMITTED, { feedback: parsed.data });
160
- return { sent: true, redacted, context };
175
+ // BACKGROUND: return now, deliver after. The agent calls this mid-task and a ~340ms POST is the
176
+ // product blocking the user's work to talk about itself. The honesty that the awaited version was
177
+ // written for is kept elsewhere: `sent` stays false (nothing is confirmed yet), `accepted` says
178
+ // the report is well-formed and queued, and a send that then fails reaches the reporter on its
179
+ // next tool result instead of vanishing.
180
+ // Written down BEFORE the network is touched, on both paths. A failed send is then a queued report
181
+ // rather than a lost one — see feedback-outbox. Reported from the field: a 1.3s hiccup destroyed a
182
+ // root-cause analysis that took an hour of driving to produce, and it survived only because the
183
+ // reporter happened to have written the markdown by hand first.
184
+ const queued = queueFeedback(parsed.data);
185
+ if (true === opts.background) {
186
+ void telemetry
187
+ .emit(TelemetryEventKind.FEEDBACK_SUBMITTED, { feedback: parsed.data })
188
+ .then((ok) => {
189
+ if (ok)
190
+ markDelivered(queued);
191
+ else
192
+ noteFeedbackUndelivered('the telemetry endpoint did not accept it');
193
+ })
194
+ .catch((error) => {
195
+ noteFeedbackUndelivered(error instanceof Error ? error.message : String(error));
196
+ });
197
+ return { sent: false, accepted: true, redacted, context };
198
+ }
199
+ // `sent` reflects DELIVERY, not handing the payload to the emitter. It was unconditional, so a DNS
200
+ // miss or a 4xx both reported "filed" — and this is the only qualitative channel the product has,
201
+ // so a silent failure here loses the report AND tells the reporter it worked.
202
+ const delivered = await telemetry.emit(TelemetryEventKind.FEEDBACK_SUBMITTED, {
203
+ feedback: parsed.data,
204
+ });
205
+ if (delivered) {
206
+ markDelivered(queued);
207
+ return { sent: true, accepted: true, redacted, context };
208
+ }
209
+ // Undelivered, but no longer lost. `accepted` is true when it reached the outbox: the report is
210
+ // well-formed and on disk, which is a materially different situation from "it is gone".
211
+ return {
212
+ sent: false,
213
+ accepted: queued !== null,
214
+ reason: queued !== null
215
+ ? `the report could not be delivered (the network call failed or was rejected), but it is SAVED at ${outboxPath()} and nothing was lost. It will be retried; you can also open an issue at https://github.com/reticlehq/reticle/issues with the same detail.`
216
+ : 'the report could not be delivered and could not be saved locally either — it was NOT filed. Retry, or open an issue at https://github.com/reticlehq/reticle/issues with the same detail.',
217
+ redacted,
218
+ context,
219
+ };
161
220
  }
162
221
  /**
163
222
  * The receipt's context PLUS the fields the emitter stamps on every event regardless of kind — the
@@ -96,7 +96,7 @@ export function gitFacts(cwd, read = defaultRead, exists = defaultExists) {
96
96
  if (url === undefined)
97
97
  return { state: GitState.LOCAL_ONLY };
98
98
  const origin = normalizeGitOrigin(url);
99
- if (origin === '')
99
+ if ('' === origin)
100
100
  return { state: GitState.LOCAL_ONLY };
101
101
  return { state: GitState.REMOTE, origin, forge: forgeOf(origin) };
102
102
  }
@@ -62,6 +62,8 @@ export declare class SessionMetrics {
62
62
  breadcrumb: string[];
63
63
  inFlight: string | undefined;
64
64
  };
65
+ /** One action driven at the page. Settled by the next verdict; see #unsettledActions. */
66
+ recordAction(): void;
65
67
  recordVerification(): void;
66
68
  /**
67
69
  * One defect found in the app under test — the outcome number, kept broken down by kind.
@@ -21,6 +21,17 @@ import { describeToolParams } from './argument-shape.js';
21
21
  import { machineSnapshot } from './machine-snapshot.js';
22
22
  /** Cap the distinct error shapes held in memory — a pathological loop must not grow unbounded. */
23
23
  const MAX_ERROR_KINDS = 40;
24
+ /** Distinct tool names whose repeat-runs are tracked. Its own cap: the tool surface, not error shapes. */
25
+ const MAX_REPEAT_TOOLS = 64;
26
+ /**
27
+ * Tool errors that all mean the same thing to a user: the agent asked, and there was no app it could
28
+ * reach. Three different messages, one experience — "nothing is connected" — and it is the error
29
+ * that ends most sessions before a single verification happens.
30
+ *
31
+ * `multiple sessions connected` belongs here too: the agent could not reach AN app because it could
32
+ * not tell which one, and the outcome is identical.
33
+ */
34
+ const NO_SESSION_ERROR = /no browser session connected|no connected session with id|multiple sessions connected/i;
24
35
  /** Cap distinct MCP clients recorded; more than a handful on one daemon is already the story. */
25
36
  const MAX_CLIENTS = 8;
26
37
  /** Bounds on the parameter map. Our own schemas are already finite; these are a belt-and-braces cap. */
@@ -71,6 +82,25 @@ export class SessionMetrics {
71
82
  * capped: the kind vocabulary is a bounded enum plus oracle names, not user data.
72
83
  */
73
84
  #seenBugKinds = new Set();
85
+ /**
86
+ * Tool errors meaning "the agent could not reach an app at all" — the single biggest drop-off in
87
+ * the funnel, and until now reachable only by unpacking `errors[]` in HogQL. 74% of daemons never
88
+ * call a tool, and of the sessions that made exactly one call, most bounced on this.
89
+ */
90
+ #noSessionErrors = 0;
91
+ /** Longest back-to-back run per tool — a retry loop, which `toolCounts` cannot distinguish. */
92
+ #repeatRuns = new Map();
93
+ #lastTool;
94
+ #currentRun = 0;
95
+ /**
96
+ * Actions driven since the last verdict — the run that is left hanging if the loop stops here.
97
+ *
98
+ * NOT `actions - verifications`. That difference ignores ORDER, so a verdict that drove nothing
99
+ * (a `flow_verify` over saved flows) silently paid for a genuinely abandoned action elsewhere, and
100
+ * a session could verify first and act after and look settled. What is abandoned is the trailing
101
+ * run, which is exactly what the field claims to mean.
102
+ */
103
+ #unsettledActions = 0;
74
104
  #clients = new Set();
75
105
  #startedAt;
76
106
  #now;
@@ -120,6 +150,16 @@ export class SessionMetrics {
120
150
  this.#unknownToolCalls += 1;
121
151
  }
122
152
  recordToolCall(tool, args) {
153
+ // A run of the same tool back to back. Five calls to reticle_act is engagement; five in a row
154
+ // after four failures is an agent stuck, and toolCounts reports both as "5".
155
+ this.#currentRun = tool === this.#lastTool ? this.#currentRun + 1 : 1;
156
+ this.#lastTool = tool;
157
+ if (this.#currentRun > 1) {
158
+ const best = this.#repeatRuns.get(tool) ?? 0;
159
+ if (this.#currentRun > best &&
160
+ (this.#repeatRuns.has(tool) || this.#repeatRuns.size < MAX_REPEAT_TOOLS))
161
+ this.#repeatRuns.set(tool, this.#currentRun);
162
+ }
123
163
  this.#toolCalls += 1;
124
164
  bump(this.#toolCounts, tool);
125
165
  this.#inFlight = tool;
@@ -150,6 +190,8 @@ export class SessionMetrics {
150
190
  */
151
191
  recordToolError(message, tool) {
152
192
  this.#toolErrors += 1;
193
+ if (NO_SESSION_ERROR.test(message))
194
+ this.#noSessionErrors += 1;
153
195
  const fingerprint = fingerprintError(message);
154
196
  const existing = this.#errorKinds.get(fingerprint);
155
197
  if (existing !== undefined) {
@@ -194,8 +236,14 @@ export class SessionMetrics {
194
236
  get trail() {
195
237
  return { breadcrumb: [...this.#breadcrumb], inFlight: this.#inFlight };
196
238
  }
239
+ /** One action driven at the page. Settled by the next verdict; see #unsettledActions. */
240
+ recordAction() {
241
+ this.#unsettledActions += 1;
242
+ }
197
243
  recordVerification() {
198
244
  this.#verifications += 1;
245
+ // A verdict settles whatever was outstanding, however many actions it took to get there.
246
+ this.#unsettledActions = 0;
199
247
  }
200
248
  /**
201
249
  * One defect found in the app under test — the outcome number, kept broken down by kind.
@@ -258,6 +306,12 @@ export class SessionMetrics {
258
306
  verifications: this.#verifications,
259
307
  bugsFound: this.#bugsFound,
260
308
  ...(this.#bugKinds.size > 0 ? { bugKinds: Object.fromEntries(this.#bugKinds) } : {}),
309
+ ...(this.#noSessionErrors > 0 ? { noSessionErrors: this.#noSessionErrors } : {}),
310
+ ...(this.#repeatRuns.size > 0
311
+ ? { consecutiveRepeats: Object.fromEntries(this.#repeatRuns) }
312
+ : {}),
313
+ // Absent rather than zero, so the PRESENCE of the field is the signal on a dashboard.
314
+ ...(this.#unsettledActions > 0 ? { abandonedActions: this.#unsettledActions } : {}),
261
315
  ...(this.#toolParams.size > 0
262
316
  ? {
263
317
  toolParams: Object.fromEntries([...this.#toolParams].map(([tool, params]) => [tool, Object.fromEntries(params)])),
@@ -277,17 +331,22 @@ export class SessionMetrics {
277
331
  }
278
332
  /** True when nothing at all happened — a flush of an idle daemon is not worth an event. */
279
333
  get empty() {
280
- return (this.#toolCalls === 0 &&
281
- this.#verifications === 0 &&
282
- this.#toolErrors === 0 &&
283
- this.#bugsFound === 0 &&
284
- this.#sdkFailures === 0);
334
+ return (0 === this.#toolCalls &&
335
+ 0 === this.#verifications &&
336
+ 0 === this.#toolErrors &&
337
+ 0 === this.#bugsFound &&
338
+ 0 === this.#sdkFailures);
285
339
  }
286
340
  /** Zero the counters after a non-final flush so the next flush reports the NEXT window, not a total. */
287
341
  reset() {
288
342
  this.#toolCalls = 0;
289
343
  this.#toolErrors = 0;
290
344
  this.#verifications = 0;
345
+ this.#noSessionErrors = 0;
346
+ this.#repeatRuns.clear();
347
+ this.#lastTool = undefined;
348
+ this.#currentRun = 0;
349
+ this.#unsettledActions = 0;
291
350
  this.#toolCounts.clear();
292
351
  this.#toolParams.clear();
293
352
  this.#errorKinds.clear();
@@ -302,7 +361,10 @@ export class SessionMetrics {
302
361
  this.#unknownToolCalls = 0;
303
362
  this.#bugsFound = 0;
304
363
  this.#bugKinds.clear();
305
- this.#seenBugKinds.clear();
364
+ // #seenBugKinds is deliberately NOT cleared. It is not a window counter — it is the
365
+ // session-lifetime memory behind `repeat` on bug_found, and zeroing it made the same defect,
366
+ // found again after a flush, report as a newly distinct one. Sessions run to 11.5 hours in the
367
+ // data, so that was up to 23 chances to count one defect many times.
306
368
  }
307
369
  }
308
370
  function bump(map, key) {
@@ -1,4 +1,20 @@
1
- import { TelemetryEventKind, type Crash, type Feedback, type Identity, type BugFound, type InitOutcome, type McpConnection, type ProjectProfile, type SessionSummary, type TelemetryActor, type Verification, type VersionChange } from '@reticlehq/core';
1
+ import { TelemetryEventKind, type Crash, type Feedback, type Identity, type BugFound, type InitOutcome, type McpConnection, type McpOutage, type ProjectProfile, type SessionSummary, type TelemetryActor, type Verification, type VersionChange } from '@reticlehq/core';
2
+ /**
3
+ * Feedback gets its own, much larger budget — and a retry.
4
+ *
5
+ * 2s + drop is right for a usage counter: the metric is worthless if it costs the user latency, and
6
+ * losing one changes nothing. Feedback is the opposite on both counts. It is the only qualitative
7
+ * channel the product has, it carries an agent's entire root-cause analysis, and losing one loses
8
+ * work that cannot be reconstructed.
9
+ *
10
+ * Measured to the collector with a WARM DNS cache: dns 0.003s, connect 0.233s, tls 0.467s, total
11
+ * 0.694s — a third of the old budget gone before any payload moves, on the good path. A fresh
12
+ * short-lived CLI process pays cold DNS and a cold route on top, which is exactly the case that
13
+ * failed in the field and then succeeded on retry from a warmer process.
14
+ */
15
+ export declare const FEEDBACK_TIMEOUT_MS = 15000;
16
+ export declare const FEEDBACK_RETRIES = 1;
17
+ export declare const FEEDBACK_RETRY_BACKOFF_MS = 750;
2
18
  /** The current telemetry state and which control set it — what `reticle telemetry status` prints. */
3
19
  export declare const describeTelemetry: (env?: NodeJS.ProcessEnv, dir?: string, cwd?: string) => {
4
20
  enabled: boolean;
@@ -38,6 +54,8 @@ export interface TelemetryExtra {
38
54
  init?: InitOutcome;
39
55
  /** `bug_found`: one defect Reticle found in the app under test. */
40
56
  bug?: BugFound;
57
+ /** `mcp_connection_lost`: the agent lost its tools — which stage, and why. */
58
+ outage?: McpOutage;
41
59
  }
42
60
  export interface Telemetry {
43
61
  /**
@@ -45,7 +63,14 @@ export interface Telemetry {
45
63
  * CLI process can exit immediately instead of waiting out the POST (use it from command entry
46
64
  * points; daemon-side events send in-process).
47
65
  */
48
- emit(kind: TelemetryEventKind, extra?: TelemetryExtra): Promise<void>;
66
+ /**
67
+ * Send one event. Resolves to whether it was DELIVERED.
68
+ *
69
+ * Almost every caller ignores the result and should — a lost metric must never surface to a user.
70
+ * `reticle_feedback` is the exception: it hands a receipt to an agent, and reporting "filed" for a
71
+ * send that failed loses the report and lies about it in the same breath.
72
+ */
73
+ emit(kind: TelemetryEventKind, extra?: TelemetryExtra): Promise<boolean>;
49
74
  readonly enabled: boolean;
50
75
  /** True the very first run on this machine — the CLI emits INSTALL alongside the first INVOKE. */
51
76
  readonly firstRun: boolean;
@@ -16,11 +16,11 @@
16
16
  */
17
17
  import { spawn } from 'node:child_process';
18
18
  import { randomUUID, createHash } from 'node:crypto';
19
- import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from 'node:fs';
19
+ import { appendFileSync, readFileSync, writeFileSync, mkdirSync, existsSync, rmSync, } from 'node:fs';
20
20
  import { homedir, platform } from 'node:os';
21
21
  import { join } from 'node:path';
22
- import { TelemetryEventKind, TELEMETRY_EVENT_VERSION, ProjectIdSource, } from '@reticlehq/core';
23
- import { SERVER_VERSION } from '../server-version.js';
22
+ import { TelemetryEventKind, TELEMETRY_EVENT_VERSION, ProjectIdSource, isSessionScoped, } from '@reticlehq/core';
23
+ import { SERVER_VERSION } from '../version/server-version.js';
24
24
  const RETICLE_DIR = join(homedir(), '.reticle');
25
25
  const ID_FILE = join(RETICLE_DIR, 'telemetry-id');
26
26
  const NOTICE_FILE = join(RETICLE_DIR, 'telemetry-notice-shown');
@@ -43,6 +43,22 @@ const POSTHOG_KEY = 'phc_q4yCxKMXHfWQ43VAz4HxJXZFLqFi4nBbg5v3avemggYo';
43
43
  */
44
44
  const POSTHOG_PERSONLESS = { $process_person_profile: false };
45
45
  const SEND_TIMEOUT_MS = 2000;
46
+ /**
47
+ * Feedback gets its own, much larger budget — and a retry.
48
+ *
49
+ * 2s + drop is right for a usage counter: the metric is worthless if it costs the user latency, and
50
+ * losing one changes nothing. Feedback is the opposite on both counts. It is the only qualitative
51
+ * channel the product has, it carries an agent's entire root-cause analysis, and losing one loses
52
+ * work that cannot be reconstructed.
53
+ *
54
+ * Measured to the collector with a WARM DNS cache: dns 0.003s, connect 0.233s, tls 0.467s, total
55
+ * 0.694s — a third of the old budget gone before any payload moves, on the good path. A fresh
56
+ * short-lived CLI process pays cold DNS and a cold route on top, which is exactly the case that
57
+ * failed in the field and then succeeded on retry from a warmer process.
58
+ */
59
+ export const FEEDBACK_TIMEOUT_MS = 15_000;
60
+ export const FEEDBACK_RETRIES = 1;
61
+ export const FEEDBACK_RETRY_BACKOFF_MS = 750;
46
62
  /**
47
63
  * The sender a `detach: true` emit runs in a disowned child (argv: [url, body]) — an in-process fetch
48
64
  * keeps Node's event loop alive until the POST finishes, which taxed every short-lived CLI command
@@ -59,6 +75,7 @@ const Env = {
59
75
  DISABLE: 'RETICLE_TELEMETRY', // "0" / "false" / "off" → disabled
60
76
  DO_NOT_TRACK: 'DO_NOT_TRACK', // the cross-tool opt-out convention (any truthy value)
61
77
  URL: 'RETICLE_TELEMETRY_URL', // override the PostHog host (EU cloud / self-hosted)
78
+ FILE: 'RETICLE_TELEMETRY_FILE', // record to a local JSONL file and send NOTHING — see the sink note
62
79
  KEY: 'RETICLE_TELEMETRY_KEY', // override the PostHog project key
63
80
  CI: 'CI', // presence marks a CI environment
64
81
  VITEST: 'VITEST', // set by vitest — unit tests must never phone home
@@ -67,6 +84,11 @@ const isDisabled = (env, cwd = process.cwd()) => {
67
84
  const off = new Set(['0', 'false', 'off', 'no']);
68
85
  if (off.has((env[Env.DISABLE] ?? '').toLowerCase()))
69
86
  return true;
87
+ // Recording to a local file is not "phoning home", and the guards below exist to stop us phoning
88
+ // home. Chief among them is the source-checkout rule — which is exactly where a release sweep is
89
+ // driven from, so a sink that inherited it would record nothing and look like it had worked.
90
+ if ((env[Env.FILE] ?? '') !== '')
91
+ return false;
70
92
  if (env[Env.VITEST] !== undefined)
71
93
  return true; // a test run is not a user
72
94
  // Developing Reticle is not using Reticle. The `.env` carrying RETICLE_TELEMETRY=0 is gitignored,
@@ -75,8 +97,21 @@ const isDisabled = (env, cwd = process.cwd()) => {
75
97
  if (isReticleSourceCheckout(cwd))
76
98
  return true;
77
99
  const dnt = env[Env.DO_NOT_TRACK];
78
- return typeof dnt === 'string' && dnt !== '' && dnt !== '0';
100
+ return 'string' === typeof dnt && dnt !== '' && dnt !== '0';
79
101
  };
102
+ /**
103
+ * Would telemetry be on here if we ignored the source-checkout guard alone?
104
+ *
105
+ * That guard exists so a fresh clone does not phone home on a contributor's first `reticle serve` —
106
+ * a rule about PASSIVE collection. Feedback is never passive: somebody typed it. Applying the guard
107
+ * to it means anyone dogfooding from their own checkout files nothing and is told "not sent, unknown
108
+ * reason", which silently loses every report from exactly the people most likely to write good ones.
109
+ * Reported from the field, and the reporter only escaped it by happening to run from another repo.
110
+ *
111
+ * Every explicit opt-out still applies — RETICLE_TELEMETRY=0, DO_NOT_TRACK, the persisted opt-out
112
+ * file, and a test run. This lifts one implicit guard for one user-initiated event kind.
113
+ */
114
+ const onlyBlockedBySourceCheckout = (env, cwd) => isDisabled(env, cwd) && !isDisabled(env, '/');
80
115
  /** Read (or mint-and-persist) the anonymous machine id. `firstRun` is true the run that created it. */
81
116
  const resolveIdentity = () => {
82
117
  try {
@@ -172,7 +207,8 @@ export const setTelemetryEnabled = (enabled, dir = RETICLE_DIR) => {
172
207
  mkdirSync(dir, { recursive: true });
173
208
  writeFileSync(optOutFile, '1', 'utf8');
174
209
  };
175
- const NOOP = { emit: async () => { }, enabled: false, firstRun: false };
210
+ // A disabled emitter delivers nothing, and says so — `false` is the honest answer, not a courtesy.
211
+ const NOOP = { emit: () => Promise.resolve(false), enabled: false, firstRun: false };
176
212
  /**
177
213
  * Build the telemetry emitter for this process. Resolves identity + opt-out ONCE; each `emit` builds a
178
214
  * fully-validated core event, maps it into PostHog capture shape, and fires a best-effort POST.
@@ -182,7 +218,9 @@ const NOOP = { emit: async () => { }, enabled: false, firstRun: false };
182
218
  export const createTelemetry = (opts) => {
183
219
  const env = opts.env ?? process.env;
184
220
  const cwd = opts.cwd ?? process.cwd();
185
- if (isDisabled(env, cwd))
221
+ // A source checkout silences metrics but must not silence FEEDBACK — see onlyBlockedBySourceCheckout.
222
+ const feedbackOnly = onlyBlockedBySourceCheckout(env, cwd);
223
+ if (isDisabled(env, cwd) && !feedbackOnly)
186
224
  return NOOP;
187
225
  try {
188
226
  if (existsSync(OPT_OUT_FILE))
@@ -192,13 +230,15 @@ export const createTelemetry = (opts) => {
192
230
  /* unreadable home dir — fall through; the env-var opt-outs above still apply */
193
231
  }
194
232
  const apiKey = env[Env.KEY] ?? POSTHOG_KEY;
195
- if (apiKey === '')
233
+ if ('' === apiKey)
196
234
  return NOOP; // no key baked in (dev/test build) — nowhere to send, stay silent
197
235
  const now = opts.now ?? (() => Date.now());
198
236
  const doFetch = opts.fetchImpl ?? fetch;
199
237
  const url = `${(env[Env.URL] ?? DEFAULT_URL).replace(/\/+$/, '')}${TELEMETRY_PATH}`;
200
238
  const ci = env[Env.CI] !== undefined && env[Env.CI] !== '';
201
239
  const os = platform();
240
+ // Negated so it reads the way people say it: UTC+2 is +120, not -120.
241
+ const tzOffsetMin = -new Date().getTimezoneOffset();
202
242
  const { anonymousId, firstRun } = resolveIdentity();
203
243
  /**
204
244
  * One id per PROCESS, minted in memory and never persisted — which is exactly right: a daemon run
@@ -212,7 +252,22 @@ export const createTelemetry = (opts) => {
212
252
  ((command, args) => {
213
253
  spawn(command, args, { detached: true, stdio: 'ignore' }).unref();
214
254
  });
255
+ /**
256
+ * Returns whether the event was actually DELIVERED.
257
+ *
258
+ * Every caller but one ignores this and should: a lost metric must never surface to a user. The
259
+ * exception is `reticle_feedback`, which hands a receipt back to an agent — `sent: true` there was
260
+ * unconditional, so a DNS miss or a 4xx both read as "filed", and the only qualitative channel the
261
+ * product has could fail silently while telling the reporter it had worked.
262
+ *
263
+ * A detached send reports false: it is handed to a disowned child precisely so the caller does not
264
+ * wait, so its outcome is genuinely unknown and claiming success would be the same lie.
265
+ */
215
266
  const emit = async (kind, extra) => {
267
+ // In a Reticle source checkout the emitter carries FEEDBACK and nothing else: somebody typed the
268
+ // feedback, and no metric is worth phoning home from a contributor's clone.
269
+ if (feedbackOnly && TelemetryEventKind.FEEDBACK_SUBMITTED !== kind)
270
+ return false;
216
271
  const event = {
217
272
  v: TELEMETRY_EVENT_VERSION,
218
273
  anonymousId,
@@ -223,6 +278,7 @@ export const createTelemetry = (opts) => {
223
278
  version: opts.version,
224
279
  ci,
225
280
  os,
281
+ tzOffsetMin,
226
282
  projectIdSource,
227
283
  ...(extra?.actor !== undefined ? { actor: extra.actor } : {}),
228
284
  ...(extra?.command !== undefined ? { command: extra.command } : {}),
@@ -237,19 +293,22 @@ export const createTelemetry = (opts) => {
237
293
  ...(extra?.connection !== undefined ? { connection: extra.connection } : {}),
238
294
  ...(extra?.init !== undefined ? { init: extra.init } : {}),
239
295
  ...(extra?.bug !== undefined ? { bug: extra.bug } : {}),
296
+ ...(extra?.outage !== undefined ? { outage: extra.outage } : {}),
240
297
  };
241
298
  // Map the core contract onto PostHog's capture shape: id/name/time move up, the rest are properties.
242
299
  // The feedback body is FLATTENED into `feedback_*` properties rather than sent as a nested object:
243
300
  // PostHog filters and breakdowns operate on top-level properties, so a nested `feedback.stack`
244
301
  // would be invisible to the exact "which stacks report the most bugs" question this exists to
245
302
  // answer. One prefix keeps them grouped in the UI without a nested path.
246
- const { anonymousId: distinctId, event: name, ts, sessionId: eventSessionId, feedback, session, project, verification, versionChange, crash, identity, connection, init, bug, ...rest } = event;
303
+ const { anonymousId: distinctId, event: name, ts, sessionId: eventSessionId, feedback, session, project, verification, versionChange, crash, identity, connection, init, bug, outage, ...rest } = event;
247
304
  // `$session_id` is PostHog's OWN session property, so sending ours under that name lights up
248
305
  // its built-in session views and funnels for free rather than requiring custom HogQL everywhere.
306
+ // `sessionId` rides only on events that happened inside a daemon run. A one-shot CLI command
307
+ // mints a per-process id that joins to nothing and inflates every session count — see
308
+ // isSessionScoped for the measurement.
249
309
  const properties = {
250
310
  ...rest,
251
- sessionId: eventSessionId,
252
- $session_id: eventSessionId,
311
+ ...(isSessionScoped(name) ? { sessionId: eventSessionId, $session_id: eventSessionId } : {}),
253
312
  };
254
313
  // Each block is flattened under its own prefix rather than nested. PostHog filters, breakdowns and
255
314
  // insight builders all operate on TOP-LEVEL properties, so a nested `project.stack` is invisible to
@@ -269,37 +328,68 @@ export const createTelemetry = (opts) => {
269
328
  connection,
270
329
  init,
271
330
  bug,
331
+ outage,
272
332
  };
273
333
  for (const [prefix, block] of Object.entries(blocks)) {
274
334
  for (const [key, value] of Object.entries(block ?? {})) {
275
335
  properties[`${prefix}_${key}`] = value;
276
336
  }
277
337
  }
278
- const body = JSON.stringify({
279
- api_key: apiKey,
280
- batch: [
281
- {
282
- event: name,
283
- distinct_id: distinctId,
284
- timestamp: new Date(ts).toISOString(),
285
- properties: { ...properties, ...POSTHOG_PERSONLESS },
286
- },
287
- ],
288
- });
338
+ const capture = {
339
+ event: name,
340
+ distinct_id: distinctId,
341
+ timestamp: new Date(ts).toISOString(),
342
+ properties: { ...properties, ...POSTHOG_PERSONLESS },
343
+ };
344
+ const body = JSON.stringify({ api_key: apiKey, batch: [capture] });
345
+ const filePath = env[Env.FILE];
346
+ if (filePath !== undefined && filePath !== '') {
347
+ // The SAME payload the wire would have carried, built by the same code path and redacted by
348
+ // the same rules — so what a run records is what a user would have sent. One JSON object per
349
+ // line, appended, so a whole sweep lands in one file in order.
350
+ try {
351
+ appendFileSync(filePath, `${JSON.stringify(capture)}\n`, 'utf8');
352
+ return true;
353
+ }
354
+ catch {
355
+ return false; // a sink that cannot write must not take the daemon down
356
+ }
357
+ }
289
358
  try {
290
- if (extra?.detach === true && opts.fetchImpl === undefined) {
359
+ if (true === extra?.detach && opts.fetchImpl === undefined) {
291
360
  spawnDetached(process.execPath, ['-e', DETACHED_SEND_SCRIPT, url, body]);
292
- return;
361
+ return false; // handed off, outcome unknowable from here
362
+ }
363
+ // Feedback is slower and retried; everything else keeps the fire-and-forget budget.
364
+ const isFeedback = TelemetryEventKind.FEEDBACK_SUBMITTED === kind;
365
+ const timeoutMs = isFeedback ? FEEDBACK_TIMEOUT_MS : SEND_TIMEOUT_MS;
366
+ const attempts = 1 + (isFeedback ? FEEDBACK_RETRIES : 0);
367
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
368
+ if (0 < attempt) {
369
+ await new Promise((r) => setTimeout(r, FEEDBACK_RETRY_BACKOFF_MS * attempt));
370
+ }
371
+ try {
372
+ const response = await doFetch(url, {
373
+ method: 'POST',
374
+ headers: { 'content-type': 'application/json' },
375
+ body,
376
+ signal: AbortSignal.timeout(timeoutMs),
377
+ });
378
+ // A 4xx is a rejected payload, not a delivery. It must not read as filed. It is also not
379
+ // worth retrying — the payload will be rejected again — so only a THROW (timeout, DNS,
380
+ // connection reset) goes round again.
381
+ return response.ok !== false;
382
+ }
383
+ catch (error) {
384
+ if (attempt === attempts - 1)
385
+ throw error;
386
+ }
293
387
  }
294
- await doFetch(url, {
295
- method: 'POST',
296
- headers: { 'content-type': 'application/json' },
297
- body,
298
- signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
299
- });
388
+ return false;
300
389
  }
301
390
  catch {
302
391
  /* best-effort: a lost metric must never surface to the user */
392
+ return false;
303
393
  }
304
394
  };
305
395
  return { emit, enabled: true, firstRun };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Which tool results count as a completed verification, as one pure rule.
3
+ *
4
+ * It used to live inline in `runTool` and read `status === 'pass'`, mapping everything else to
5
+ * "no verdict". That was wrong in both directions at once, and the shape of the error is visible in
6
+ * the production data:
7
+ *
8
+ * - a FAILING suite emitted nothing, so `verification_completed` only ever fired on greens while
9
+ * `bugsInResult` fired on the reds — bugs with no verifications to divide them by;
10
+ * - an EMPTY suite ("all 0 flows pass") emitted `verified: yes, passed: true`.
11
+ *
12
+ * Extracted because a rule this easy to get wrong, feeding the one number shown to investors, should
13
+ * be readable and testable on its own rather than inferred from a ternary inside a dispatcher.
14
+ */
15
+ import { type BrowserBrand, type Verification } from '@reticlehq/core';
16
+ /**
17
+ * The verification payload for a result, or undefined when nothing was verified.
18
+ *
19
+ * `falseGreenCaught` is the one that matters: the assertion PASSED and Reticle still refused to call
20
+ * it verified. That is the product's thesis reduced to a boolean.
21
+ */
22
+ export declare function verificationOf(toolName: string, result: Record<string, unknown>, durationMs: number, brand?: BrowserBrand): Verification | undefined;