@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
package/dist/trace.js ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Verbose internal flow tracing, for people working ON Reticle.
3
+ *
4
+ * The gap it fills: the journal records what the AGENT did to the app, and telemetry records
5
+ * aggregates. Neither says which of OUR code a tool call went through, in what order, or where the
6
+ * time went — so every "why is this flow slow" and "which path produced this verdict" was
7
+ * reconstructed by hand from reading source. One line per stage answers both.
8
+ *
9
+ * Design, in order of what it cost to get wrong elsewhere in this repo:
10
+ *
11
+ * - OFF by default, and ~126ns per site when off (measured) — `span` calls the function and
12
+ * returns. A trace on every tool call is a real cost on the hot path, and the hot path is a
13
+ * verification loop of 50–200 calls.
14
+ * - ONE line per stage, emitted when it ENDS, carrying its own duration. A start line as well
15
+ * would double the volume to say something the end line already implies.
16
+ * - Correlated by a call id and a depth, carried in AsyncLocalStorage rather than threaded through
17
+ * every signature. Several agents are inside `runTool` at once — interleaving is the normal case
18
+ * — so without an id the output is a pile of unrelated lines, and instrumenting anything would
19
+ * mean changing its parameters, which is how instrumentation stops being added.
20
+ * - Through the existing `log()`, so it lands in the same stderr stream the daemon already
21
+ * redirects to `~/.reticle/daemon-<port>.log`. No new sink, no new file, nothing else to tail.
22
+ */
23
+ import { AsyncLocalStorage } from 'node:async_hooks';
24
+ import { ReticleEnv } from '@reticlehq/core';
25
+ import { log } from './log.js';
26
+ /** The `event` field every trace line carries, so one `grep` separates it from milestone logs. */
27
+ const TRACE_EVENT = 'trace';
28
+ const ON_VALUES = new Set(['1', 'true', 'on', 'yes']);
29
+ const context = new AsyncLocalStorage();
30
+ /**
31
+ * Monotonic within a process, and PID-qualified so it stays unique across them.
32
+ *
33
+ * A bare counter was not enough: the e2e battery runs many daemons, and a restarted daemon starts
34
+ * over at 1, so `c7` named a different call in every process. Aggregating that trace silently
35
+ * merged unrelated calls into one — measured on the first real trace collected, before it had a
36
+ * chance to mislead anything.
37
+ */
38
+ let nextCallId = 0;
39
+ const CALL_ID_PREFIX = `p${String(process.pid)}-c`;
40
+ /**
41
+ * Whether tracing is on.
42
+ *
43
+ * Read per call, not cached at import, and the reason is `loadDotEnv`: the CLI populates
44
+ * `process.env` from the project's .env AFTER every module has been imported, so a value captured at
45
+ * import time would miss a `RETICLE_TRACE` set there — silently, which is the worst way for a
46
+ * diagnostic switch to fail. (The reason originally given here — "so it can be turned on while the
47
+ * daemon runs" — was wrong: nothing outside a process can change its environment.)
48
+ *
49
+ * Measured cost of reading it per call: 126ns per traced site when tracing is OFF, against a ~9ns
50
+ * bare call. With a handful of spans per tool call that is under a microsecond on a call that takes
51
+ * milliseconds — cheap enough to be worth the correctness, and not the literal zero this comment
52
+ * used to imply.
53
+ */
54
+ export function traceEnabled() {
55
+ const raw = process.env[ReticleEnv.TRACE];
56
+ return raw !== undefined && ON_VALUES.has(raw.trim().toLowerCase());
57
+ }
58
+ /** The scope a stage runs in, plus how to report it. Shared by the async and sync forms. */
59
+ function openSpan(name, fields) {
60
+ const parent = context.getStore();
61
+ const scope = parent === undefined
62
+ ? { callId: `${CALL_ID_PREFIX}${String((nextCallId += 1))}`, depth: 0 }
63
+ : parent;
64
+ const depth = parent === undefined ? 0 : parent.depth;
65
+ const startedAt = Date.now();
66
+ return {
67
+ // The child scope is what nested spans see: same call, one level deeper.
68
+ child: { callId: scope.callId, depth: depth + 1 },
69
+ emit: (ok, extra) => {
70
+ log(TRACE_EVENT, {
71
+ span: name,
72
+ ms: Date.now() - startedAt,
73
+ depth,
74
+ callId: scope.callId,
75
+ ok,
76
+ ...fields,
77
+ ...extra,
78
+ });
79
+ },
80
+ };
81
+ }
82
+ /**
83
+ * Bind a callback to the call that is running NOW, so it reports inside that call when it fires later.
84
+ *
85
+ * `waitForPredicate` re-checks its predicate from a WebSocket event listener and an interval. Neither
86
+ * is in the awaited chain, so AsyncLocalStorage does not reach them and every re-check used to open a
87
+ * brand new call at depth 0. Measured on one healthy run: 165 spans across 60 callIds, 25 of them a
88
+ * `browser.command` with no matching `tool.handler`.
89
+ *
90
+ * That is not cosmetic. The documented signature of a HUNG tool call is exactly "a browser.command
91
+ * with no tool.handler for that callId, ever" — the invariant that located a real hang. A clean run
92
+ * emitting 23 of them makes the invariant unusable and hides the next hang in its own noise.
93
+ *
94
+ * Returns `fn` unchanged when there is no call to bind to, so a caller outside a span is not given a
95
+ * fabricated parent.
96
+ */
97
+ export function bindSpanContext(fn) {
98
+ const captured = context.getStore();
99
+ if (captured === undefined)
100
+ return fn;
101
+ return (...args) => {
102
+ context.run(captured, () => fn(...args));
103
+ };
104
+ }
105
+ /** How much of a thrown value to keep. Enough to identify it, not enough to paste a stack into a log. */
106
+ const ERROR_CHARS = 300;
107
+ /**
108
+ * Run `fn` as a named stage, recording how long it took and how it sat inside the call around it.
109
+ *
110
+ * Wrap a stage worth a line in a bottleneck hunt — a session resolve, a browser round-trip, a
111
+ * predicate evaluation — not every function. The value of the trace is that reading it is faster
112
+ * than reading the code, and that stops being true somewhere around one line per statement.
113
+ */
114
+ export async function span(name, fields, fn) {
115
+ if (!traceEnabled())
116
+ return fn();
117
+ const { child, emit } = openSpan(name, fields);
118
+ try {
119
+ const value = await context.run(child, async () => fn());
120
+ emit(true, {});
121
+ return value;
122
+ }
123
+ catch (err) {
124
+ // A stage that THREW is the one most worth having in the trace: without this line the trace
125
+ // shows a call that entered a stage and never left it, which reads as a hang.
126
+ emit(false, { error: String(err).slice(0, ERROR_CHARS) });
127
+ throw err;
128
+ }
129
+ }
130
+ /**
131
+ * The synchronous form. Same line, same tree, same cost when off.
132
+ *
133
+ * It exists because `reticle init` — the flow every user hits before anything else, and the one
134
+ * whose slowness they experience personally — is synchronous end to end. An async-only span left
135
+ * exactly that flow untraceable.
136
+ */
137
+ export function spanSync(name, fields, fn) {
138
+ if (!traceEnabled())
139
+ return fn();
140
+ const { child, emit } = openSpan(name, fields);
141
+ try {
142
+ const value = context.run(child, fn);
143
+ emit(true, {});
144
+ return value;
145
+ }
146
+ catch (err) {
147
+ emit(false, { error: String(err).slice(0, ERROR_CHARS) });
148
+ throw err;
149
+ }
150
+ }
@@ -0,0 +1,4 @@
1
+ /** Record that an agent was told about `version`. Called when the nudge is actually delivered. */
2
+ export declare function creditNudge(version: string, dir?: string, now?: number): void;
3
+ /** True when this exact version was offered to an agent recently enough to have caused the update. */
4
+ export declare function wasNudged(version: string, dir?: string, now?: number): boolean;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Did the update nudge cause the update?
3
+ *
4
+ * `version_changed` says a version moved. It has never said why, so the only question worth asking
5
+ * about the nudge — does telling agents about a release make releases get installed — was
6
+ * unanswerable. That is not academic: 2.4.0 carried a fix for a connect defect affecting every Vite
7
+ * app and reached zero users, and nothing in the data could distinguish "the nudge never fired" from
8
+ * "the nudge fired and nobody acted on it". Those need opposite responses.
9
+ *
10
+ * The nudge is delivered by a DAEMON; `reticle update` runs in a different process. They cannot see
11
+ * each other in memory, so a marker file is the join. It holds the version that was offered and
12
+ * when — no identity, nothing about the machine, and it is overwritten rather than appended.
13
+ *
14
+ * Every function here swallows its own errors. This runs inside `reticle update`, which must finish
15
+ * whatever the telemetry believes.
16
+ */
17
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
18
+ import { homedir } from 'node:os';
19
+ import { join } from 'node:path';
20
+ const DEFAULT_DIR = join(homedir(), '.reticle');
21
+ const FILE = 'update-nudge.json';
22
+ /**
23
+ * How long a nudge can plausibly have caused an update. A week is generous for "the agent mentioned
24
+ * it and the human got round to it"; beyond that the two are unrelated events that happen to share a
25
+ * version number.
26
+ */
27
+ const CREDIT_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
28
+ /** Record that an agent was told about `version`. Called when the nudge is actually delivered. */
29
+ export function creditNudge(version, dir = DEFAULT_DIR, now = Date.now()) {
30
+ try {
31
+ mkdirSync(dir, { recursive: true });
32
+ const record = { offered: version, at: now };
33
+ writeFileSync(join(dir, FILE), JSON.stringify(record), 'utf8');
34
+ }
35
+ catch {
36
+ /* a metric must never break the nudge */
37
+ }
38
+ }
39
+ /** True when this exact version was offered to an agent recently enough to have caused the update. */
40
+ export function wasNudged(version, dir = DEFAULT_DIR, now = Date.now()) {
41
+ try {
42
+ const parsed = JSON.parse(readFileSync(join(dir, FILE), 'utf8'));
43
+ if (typeof parsed !== 'object' || null === parsed)
44
+ return false;
45
+ const record = parsed;
46
+ if (record.offered !== version || typeof record.at !== 'number')
47
+ return false;
48
+ return now - record.at <= CREDIT_WINDOW_MS;
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The half of `reticle update` that was missing: bringing the APP's SDK to the same version as the
3
+ * daemon being installed.
4
+ *
5
+ * `reticle update` swapped the CLI and stopped. The app kept whatever `@reticlehq/react` it had, so
6
+ * the command whose job is to keep an install current was itself a way to create a version-skewed
7
+ * pair — the failure the HELLO skew check, the nudge and the pin all exist to prevent. The skew
8
+ * message even told people to run `reticle update` to fix an outdated SDK, which could not work.
9
+ *
10
+ * Pinned to an exact version for the same reason `init` pins: an unpinned `add` can resolve out of a
11
+ * stale registry cache and reinstall the skew being fixed. Measured once as pnpm taking 2.2.1 while
12
+ * npm took 2.3.0 in the next project over.
13
+ */
14
+ import { type PackageManager } from '../init/detect.js';
15
+ /** The Reticle packages this project declares, from either dependency section. */
16
+ export declare function reticleDepsOf(pkgJson: unknown): string[];
17
+ /** The install to run, or null when this project has nothing of ours to sync. */
18
+ export declare function sdkSyncCommand(pm: PackageManager, packages: readonly string[], version: string): {
19
+ command: string;
20
+ args: string[];
21
+ } | null;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The half of `reticle update` that was missing: bringing the APP's SDK to the same version as the
3
+ * daemon being installed.
4
+ *
5
+ * `reticle update` swapped the CLI and stopped. The app kept whatever `@reticlehq/react` it had, so
6
+ * the command whose job is to keep an install current was itself a way to create a version-skewed
7
+ * pair — the failure the HELLO skew check, the nudge and the pin all exist to prevent. The skew
8
+ * message even told people to run `reticle update` to fix an outdated SDK, which could not work.
9
+ *
10
+ * Pinned to an exact version for the same reason `init` pins: an unpinned `add` can resolve out of a
11
+ * stale registry cache and reinstall the skew being fixed. Measured once as pnpm taking 2.2.1 while
12
+ * npm took 2.3.0 in the next project over.
13
+ */
14
+ import { installCommandParts } from '../init/detect.js';
15
+ /** Everything we publish is scoped here. */
16
+ const RETICLE_SCOPE = '@reticlehq/';
17
+ /**
18
+ * The CLI. Never an app dependency: it carries the Node MCP server and `ws`, and putting it in a
19
+ * browser app's tree is what the retired `@reticlehq/core` umbrella got wrong. The other half of
20
+ * `reticle update` owns this one.
21
+ */
22
+ const SERVER_PACKAGE = '@reticlehq/server';
23
+ function namesIn(section) {
24
+ if ('object' !== typeof section || null === section)
25
+ return [];
26
+ return Object.keys(section);
27
+ }
28
+ /** The Reticle packages this project declares, from either dependency section. */
29
+ export function reticleDepsOf(pkgJson) {
30
+ if ('object' !== typeof pkgJson || null === pkgJson)
31
+ return [];
32
+ const manifest = pkgJson;
33
+ const declared = [...namesIn(manifest['dependencies']), ...namesIn(manifest['devDependencies'])];
34
+ return [...new Set(declared)].filter((name) => name.startsWith(RETICLE_SCOPE) && name !== SERVER_PACKAGE);
35
+ }
36
+ /** The install to run, or null when this project has nothing of ours to sync. */
37
+ export function sdkSyncCommand(pm, packages, version) {
38
+ if (0 === packages.length)
39
+ return null;
40
+ const { command, args } = installCommandParts(pm, packages.map((p) => `${p}@${version}`));
41
+ return { command, args: [...args] };
42
+ }
@@ -3,7 +3,7 @@ import * as https from 'node:https';
3
3
  import { join } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
5
  import { UpdateCheckIntervalMs } from '@reticlehq/core';
6
- import { RETICLE_NPM_PACKAGE } from '../server-version.js';
6
+ import { RETICLE_NPM_PACKAGE } from '../version/server-version.js';
7
7
  import { log } from '../log.js';
8
8
  const RETICLE_HOME = join(homedir(), '.reticle');
9
9
  const MANIFEST_PATH = join(RETICLE_HOME, 'update-manifest.json');
@@ -20,12 +20,12 @@ function validateNpmInfo(info) {
20
20
  if (typeof info.version !== 'string' || !VERSION_RE.test(info.version)) {
21
21
  throw new Error('npm registry returned an implausible version');
22
22
  }
23
- const changelog = typeof info.reticle?.changelog === 'string'
23
+ const changelog = 'string' === typeof info.reticle?.changelog
24
24
  ? info.reticle.changelog.slice(0, MAX_MANIFEST_TEXT)
25
25
  : undefined;
26
26
  const breakingChanges = Array.isArray(info.reticle?.breakingChanges)
27
27
  ? info.reticle.breakingChanges
28
- .filter((x) => typeof x === 'string')
28
+ .filter((x) => 'string' === typeof x)
29
29
  .slice(0, 100)
30
30
  .map((s) => s.slice(0, MAX_MANIFEST_TEXT))
31
31
  : undefined;
@@ -1,4 +1,14 @@
1
- export interface UpdateNudge {
1
+ /**
2
+ * Is `candidate` a strictly NEWER release than `current`?
3
+ *
4
+ * Replaces a plain `!==`, which told anyone on a version newer than the published one — a
5
+ * prerelease, a local build, a rollback in progress — to "update" to something older. A nudge that
6
+ * prompts a downgrade is one people learn to ignore, and this is the mechanism the entire adoption
7
+ * story rests on. Numeric per segment, so 2.10.0 correctly beats 2.9.0 (string order does not), and
8
+ * a bare release beats its own prerelease.
9
+ */
10
+ export declare function isNewerVersion(candidate: string, current: string): boolean;
11
+ interface UpdateNudge {
2
12
  /** The version available, so the agent can tell the human what they would be moving to. */
3
13
  latestVersion: string;
4
14
  currentVersion: string;
@@ -7,14 +17,61 @@ export interface UpdateNudge {
7
17
  action: string;
8
18
  }
9
19
  /**
10
- * Kick off a background check. Non-blocking and best-effort: `checkForUpdate` never throws, caches
11
- * for 24h on disk, and falls back to the cached manifest when the registry is unreachable — so this
12
- * costs one npm registry request per day per machine, and nothing at all when offline.
20
+ * Describe the release, not just its number.
21
+ *
22
+ * `packages/server/package.json` carries `reticle.changelog` and `reticle.breakingChanges`, and
23
+ * `update-checker` parses both into the manifest — where they stopped. This function named two
24
+ * versions and nothing else, so both fields were written, shipped, parsed and discarded. Latent
25
+ * metadata nobody ever printed.
26
+ *
27
+ * Survivable while releases are additive; actively harmful for one that is not. An agent told only
28
+ * "2.4.1 → 2.5.0" runs `reticle update` mid-task and discovers a retired environment variable and
29
+ * six newly-strict parameters by breaking on them.
30
+ *
31
+ * Bounded on purpose: this rides on a tool result every turn until it is delivered, so a long
32
+ * changelog is a per-turn tax. The count of anything elided is stated rather than silently dropped.
13
33
  */
34
+ export declare function buildNudge(latestVersion: string, currentVersion: string, release?: {
35
+ changelog?: string;
36
+ breakingChanges?: string[];
37
+ }): UpdateNudge;
14
38
  export declare function startUpdateCheck(now?: () => number): void;
39
+ /**
40
+ * What `reticle update` should install, or undefined when it should do nothing.
41
+ *
42
+ * `handleUpdate` gated on `manifest.updateAvailable`, a plain `latest !== current`, so whenever the
43
+ * registry's latest was OLDER than the running build — a prerelease, a local build, a rollback in
44
+ * progress, a stale npx cache — it announced the move and then installed the downgrade. Reported by
45
+ * a real user on 2026-08-06: "both the `update_available` banner and `reticle update` report the
46
+ * current and target versions swapped, so an upgrade is described as a downgrade."
47
+ */
48
+ export declare function updateTarget(manifest: {
49
+ updateAvailable?: boolean;
50
+ latestVersion?: string;
51
+ }, currentVersion?: string): string | undefined;
52
+ /**
53
+ * Arm from an already-cached answer, synchronously, at daemon boot.
54
+ *
55
+ * The network check fires 8s after boot and the nudge is delivered by riding a tool result — but
56
+ * half the sessions that use Reticle at all make exactly ONE tool call, usually in the first
57
+ * seconds. The check had not finished, so the single chance to tell them was gone, and the
58
+ * population most in need of the update is precisely the one that never heard about it. Yesterday's
59
+ * answer is already on disk; use it now and let the network refresh behind it.
60
+ */
61
+ export declare function armUpdateNudgeFrom(cached: {
62
+ latestVersion?: string;
63
+ updateAvailable?: boolean;
64
+ changelog?: string;
65
+ breakingChanges?: string[];
66
+ } | null, currentVersion?: string): void;
15
67
  /** The nudge, once. Returns undefined afterwards so a long session is told exactly one time. */
16
68
  export declare function takeUpdateNudge(): UpdateNudge | undefined;
69
+ /**
70
+ * The newer version, if the cached answer says there is one — WITHOUT consuming the agent's
71
+ * one-shot nudge. `reticle status` is a human reading a terminal; the two channels are separate
72
+ * audiences and must not steal each other's message.
73
+ */
74
+ export declare function availableUpdate(currentVersion?: string): string | undefined;
17
75
  /** Tests only — drop the module state so each case starts clean. */
18
76
  export declare function resetUpdateNudge(): void;
19
- /** Tests only — seed a pending nudge without going near the network. */
20
- export declare function setPendingUpdate(nudge: UpdateNudge | undefined): void;
77
+ export {};
@@ -16,9 +16,35 @@
16
16
  * human never agreed to interrupt. So the envelope carries the fact and the exact command, and the
17
17
  * decision stays with the people whose machine it is.
18
18
  */
19
- import { checkForUpdate } from './update-checker.js';
20
- import { SERVER_VERSION } from '../server-version.js';
19
+ import { checkForUpdate, loadManifest } from './update-checker.js';
20
+ import { SERVER_VERSION } from '../version/server-version.js';
21
21
  import { log } from '../log.js';
22
+ import { creditNudge } from './nudge-credit.js';
23
+ /**
24
+ * Is `candidate` a strictly NEWER release than `current`?
25
+ *
26
+ * Replaces a plain `!==`, which told anyone on a version newer than the published one — a
27
+ * prerelease, a local build, a rollback in progress — to "update" to something older. A nudge that
28
+ * prompts a downgrade is one people learn to ignore, and this is the mechanism the entire adoption
29
+ * story rests on. Numeric per segment, so 2.10.0 correctly beats 2.9.0 (string order does not), and
30
+ * a bare release beats its own prerelease.
31
+ */
32
+ export function isNewerVersion(candidate, current) {
33
+ const parse = (v) => {
34
+ const [core = '', ...rest] = v.split(/[-+]/);
35
+ return { parts: core.split('.').map((n) => Number.parseInt(n, 10) || 0), pre: rest.length > 0 };
36
+ };
37
+ const a = parse(candidate);
38
+ const b = parse(current);
39
+ for (let i = 0; i < 3; i++) {
40
+ const left = a.parts[i] ?? 0;
41
+ const right = b.parts[i] ?? 0;
42
+ if (left !== right)
43
+ return left > right;
44
+ }
45
+ // Same numbers: a final release is newer than its own prerelease, and nothing else counts.
46
+ return !a.pre && b.pre;
47
+ }
22
48
  /** Let the daemon finish coming up before touching the network. */
23
49
  const CHECK_DELAY_MS = 8_000;
24
50
  const UPDATE_COMMAND = 'reticle update';
@@ -29,20 +55,63 @@ let delivered = false;
29
55
  * for 24h on disk, and falls back to the cached manifest when the registry is unreachable — so this
30
56
  * costs one npm registry request per day per machine, and nothing at all when offline.
31
57
  */
58
+ /** How much of the release description may ride on a tool result. See buildNudge. */
59
+ const MAX_CHANGELOG_CHARS = 240;
60
+ const MAX_LISTED_BREAKING = 4;
61
+ /**
62
+ * Describe the release, not just its number.
63
+ *
64
+ * `packages/server/package.json` carries `reticle.changelog` and `reticle.breakingChanges`, and
65
+ * `update-checker` parses both into the manifest — where they stopped. This function named two
66
+ * versions and nothing else, so both fields were written, shipped, parsed and discarded. Latent
67
+ * metadata nobody ever printed.
68
+ *
69
+ * Survivable while releases are additive; actively harmful for one that is not. An agent told only
70
+ * "2.4.1 → 2.5.0" runs `reticle update` mid-task and discovers a retired environment variable and
71
+ * six newly-strict parameters by breaking on them.
72
+ *
73
+ * Bounded on purpose: this rides on a tool result every turn until it is delivered, so a long
74
+ * changelog is a per-turn tax. The count of anything elided is stated rather than silently dropped.
75
+ */
76
+ export function buildNudge(latestVersion, currentVersion, release) {
77
+ const changelog = (release?.changelog ?? '').trim();
78
+ const breaking = (release?.breakingChanges ?? []).filter((line) => 0 < line.trim().length);
79
+ const listed = breaking.slice(0, MAX_LISTED_BREAKING);
80
+ const rest = breaking.length - listed.length;
81
+ return {
82
+ currentVersion,
83
+ latestVersion,
84
+ command: UPDATE_COMMAND,
85
+ action: `A newer Reticle is available (${currentVersion} → ${latestVersion}). ` +
86
+ ('' === changelog ? '' : `${changelog.slice(0, MAX_CHANGELOG_CHARS)} `) +
87
+ (0 === breaking.length
88
+ ? ''
89
+ : `This release has BREAKING changes: ${listed.join('; ')}` +
90
+ (0 < rest ? ` (+${String(rest)} more — see the changelog)` : '') +
91
+ '. Read them before updating. ') +
92
+ `Tell the human, and run \`${UPDATE_COMMAND}\` if they agree — it restarts the daemon, ` +
93
+ 'so do it between tasks rather than mid-verification. Continue your current task first.',
94
+ };
95
+ }
32
96
  export function startUpdateCheck(now = () => Date.now()) {
97
+ // The cached answer first, so the very first tool call can carry it — see armUpdateNudgeFrom.
98
+ armUpdateNudgeFrom(loadManifest());
33
99
  setTimeout(() => {
34
100
  void checkForUpdate(SERVER_VERSION, now)
35
101
  .then((manifest) => {
36
102
  if (!manifest.updateAvailable || manifest.latestVersion === undefined)
37
103
  return;
38
- pending = {
39
- currentVersion: SERVER_VERSION,
40
- latestVersion: manifest.latestVersion,
41
- command: UPDATE_COMMAND,
42
- action: `A newer Reticle is available (${SERVER_VERSION} → ${manifest.latestVersion}). ` +
43
- `Tell the human, and run \`${UPDATE_COMMAND}\` if they agree — it restarts the daemon, ` +
44
- 'so do it between tasks rather than mid-verification. Continue your current task first.',
45
- };
104
+ if (!isNewerVersion(manifest.latestVersion, SERVER_VERSION))
105
+ return;
106
+ // WITH the release description. The manifest has carried `changelog` and `breakingChanges`
107
+ // all along; nothing read them.
108
+ pending = buildNudge(manifest.latestVersion, SERVER_VERSION, {
109
+ ...(manifest.changelog === undefined ? {} : { changelog: manifest.changelog }),
110
+ ...(manifest.breakingChanges === undefined
111
+ ? {}
112
+ : { breakingChanges: manifest.breakingChanges }),
113
+ });
114
+ delivered = false;
46
115
  log('reticle_update_available', {
47
116
  from: SERVER_VERSION,
48
117
  to: manifest.latestVersion,
@@ -53,20 +122,64 @@ export function startUpdateCheck(now = () => Date.now()) {
53
122
  });
54
123
  }, CHECK_DELAY_MS).unref();
55
124
  }
125
+ /**
126
+ * What `reticle update` should install, or undefined when it should do nothing.
127
+ *
128
+ * `handleUpdate` gated on `manifest.updateAvailable`, a plain `latest !== current`, so whenever the
129
+ * registry's latest was OLDER than the running build — a prerelease, a local build, a rollback in
130
+ * progress, a stale npx cache — it announced the move and then installed the downgrade. Reported by
131
+ * a real user on 2026-08-06: "both the `update_available` banner and `reticle update` report the
132
+ * current and target versions swapped, so an upgrade is described as a downgrade."
133
+ */
134
+ export function updateTarget(manifest, currentVersion = SERVER_VERSION) {
135
+ const latest = manifest.latestVersion;
136
+ if (latest === undefined || !isNewerVersion(latest, currentVersion))
137
+ return undefined;
138
+ return latest;
139
+ }
140
+ /**
141
+ * Arm from an already-cached answer, synchronously, at daemon boot.
142
+ *
143
+ * The network check fires 8s after boot and the nudge is delivered by riding a tool result — but
144
+ * half the sessions that use Reticle at all make exactly ONE tool call, usually in the first
145
+ * seconds. The check had not finished, so the single chance to tell them was gone, and the
146
+ * population most in need of the update is precisely the one that never heard about it. Yesterday's
147
+ * answer is already on disk; use it now and let the network refresh behind it.
148
+ */
149
+ export function armUpdateNudgeFrom(cached, currentVersion = SERVER_VERSION) {
150
+ const latest = cached?.latestVersion;
151
+ if (latest === undefined || !isNewerVersion(latest, currentVersion))
152
+ return;
153
+ // The CACHED path matters more than the live one: it is what the very first tool call carries, and
154
+ // it is the only nudge an offline machine ever sees. Dropping the release description here would
155
+ // have left the fields unread on the commonest path while looking fixed.
156
+ pending = buildNudge(latest, currentVersion, {
157
+ ...(cached?.changelog === undefined ? {} : { changelog: cached.changelog }),
158
+ ...(cached?.breakingChanges === undefined ? {} : { breakingChanges: cached.breakingChanges }),
159
+ });
160
+ delivered = false;
161
+ }
56
162
  /** The nudge, once. Returns undefined afterwards so a long session is told exactly one time. */
57
163
  export function takeUpdateNudge() {
58
164
  if (delivered || pending === undefined)
59
165
  return undefined;
60
166
  delivered = true;
167
+ // Leave a mark an `reticle update` in another process can read, so version_changed can say whether
168
+ // this nudge is what caused the upgrade. Best-effort by construction — see nudge-credit.
169
+ creditNudge(pending.latestVersion);
61
170
  return pending;
62
171
  }
172
+ /**
173
+ * The newer version, if the cached answer says there is one — WITHOUT consuming the agent's
174
+ * one-shot nudge. `reticle status` is a human reading a terminal; the two channels are separate
175
+ * audiences and must not steal each other's message.
176
+ */
177
+ export function availableUpdate(currentVersion = SERVER_VERSION) {
178
+ const latest = loadManifest()?.latestVersion;
179
+ return latest !== undefined && isNewerVersion(latest, currentVersion) ? latest : undefined;
180
+ }
63
181
  /** Tests only — drop the module state so each case starts clean. */
64
182
  export function resetUpdateNudge() {
65
183
  pending = undefined;
66
184
  delivered = false;
67
185
  }
68
- /** Tests only — seed a pending nudge without going near the network. */
69
- export function setPendingUpdate(nudge) {
70
- pending = nudge;
71
- delivered = false;
72
- }
@@ -1,5 +1,5 @@
1
1
  /** Which way the installed version moved. A rollback means a release hurt someone enough to retreat. */
2
- export declare const VersionChangeDirection: {
2
+ declare const VersionChangeDirection: {
3
3
  readonly UPDATE: "update";
4
4
  readonly ROLLBACK: "rollback";
5
5
  };
@@ -22,14 +22,6 @@ declare const ExecutionKind: {
22
22
  readonly LOCAL: "local";
23
23
  };
24
24
  type ExecutionKind = (typeof ExecutionKind)[keyof typeof ExecutionKind];
25
- /**
26
- * Infer how reticle was launched from process.argv[1].
27
- *
28
- * npm's npx cache stores packages under a path containing `_npx`, which is the
29
- * most reliable cross-platform signal. Local installs always live inside a
30
- * `node_modules` directory. Everything else is treated as a global install.
31
- */
32
- export declare function detectExecutionKind(): ExecutionKind;
33
25
  /**
34
26
  * Classify a launch from the entry script path. Pure so it's testable. Global installs ALSO live under
35
27
  * a `node_modules`, but under the npm global prefix — a `lib/node_modules` (unix: /usr/local, Homebrew,
@@ -1,14 +1,16 @@
1
1
  import { execFile } from 'node:child_process';
2
+ import { NodePlatform } from '../platform.js';
2
3
  import { existsSync } from 'node:fs';
3
4
  import { platform } from 'node:os';
4
5
  import { dirname, join } from 'node:path';
5
6
  import { loadManifest, saveManifest } from './update-checker.js';
6
- import { RETICLE_NPM_PACKAGE, SERVER_VERSION } from '../server-version.js';
7
+ import { RETICLE_NPM_PACKAGE, SERVER_VERSION } from '../version/server-version.js';
7
8
  import { log } from '../log.js';
8
9
  import { TelemetryEventKind } from '@reticlehq/core';
9
10
  import { getTelemetry } from '../telemetry/telemetry.js';
11
+ import { wasNudged } from './nudge-credit.js';
10
12
  /** Which way the installed version moved. A rollback means a release hurt someone enough to retreat. */
11
- export const VersionChangeDirection = {
13
+ const VersionChangeDirection = {
12
14
  UPDATE: 'update',
13
15
  ROLLBACK: 'rollback',
14
16
  };
@@ -21,15 +23,18 @@ export const VersionChangeDirection = {
21
23
  */
22
24
  export async function reportVersionChange(from, to, direction) {
23
25
  try {
26
+ const nudged = wasNudged(to);
24
27
  await getTelemetry().emit(TelemetryEventKind.VERSION_CHANGED, {
25
- versionChange: { from, to, direction },
28
+ // Only when true: an absent flag reads as "we do not know", which is honest for a machine that
29
+ // updated from a shell script or a package bump with no agent involved at all.
30
+ versionChange: { from, to, direction, ...(nudged ? { nudged } : {}) },
26
31
  });
27
32
  }
28
33
  catch {
29
34
  /* best-effort, like every other send */
30
35
  }
31
36
  }
32
- const NPM_BIN = platform() === 'win32' ? 'npm.cmd' : 'npm';
37
+ const NPM_BIN = NodePlatform.WINDOWS === platform() ? 'npm.cmd' : 'npm';
33
38
  const NPM_TIMEOUT_MS = 120_000;
34
39
  /** How this reticle process was launched — determines which npm strategy to use for updates. */
35
40
  const ExecutionKind = {
@@ -47,7 +52,7 @@ const ExecutionKind = {
47
52
  * most reliable cross-platform signal. Local installs always live inside a
48
53
  * `node_modules` directory. Everything else is treated as a global install.
49
54
  */
50
- export function detectExecutionKind() {
55
+ function detectExecutionKind() {
51
56
  return classifyExecutionKind(process.argv[1] ?? '');
52
57
  }
53
58
  /**
@@ -116,7 +121,7 @@ export function installArgs(version, kind, localRoot) {
116
121
  async function installVersion(version, kind) {
117
122
  const localRoot = kind === ExecutionKind.LOCAL ? findLocalProjectRoot() : null;
118
123
  const plan = installArgs(version, kind, localRoot);
119
- if (plan === null) {
124
+ if (null === plan) {
120
125
  // npx re-resolves the package from npm on the next Claude Code restart — no npm
121
126
  // install needed. The restart itself is what triggers the update.
122
127
  log('reticle_update_npx_strategy', {
@@ -124,7 +129,7 @@ async function installVersion(version, kind) {
124
129
  });
125
130
  return;
126
131
  }
127
- if (kind === ExecutionKind.LOCAL && localRoot === null) {
132
+ if (kind === ExecutionKind.LOCAL && null === localRoot) {
128
133
  // Could not find a project root — fell back to a global install as a safe default.
129
134
  log('reticle_update_local_no_root', { fallback: 'global' });
130
135
  }
@@ -166,7 +171,7 @@ export async function applyUpdate(targetVersion) {
166
171
  */
167
172
  export async function rollback() {
168
173
  const manifest = loadManifest();
169
- if (manifest === null || manifest.previousVersion === undefined) {
174
+ if (null === manifest || manifest.previousVersion === undefined) {
170
175
  throw new Error('No previous version available for rollback');
171
176
  }
172
177
  const prev = manifest.previousVersion;