@reticlehq/server 2.12.0 → 2.13.1

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 (386) hide show
  1. package/SKILL.md +67 -109
  2. package/dist/bridge/auth-failure-reason.d.ts +6 -2
  3. package/dist/bridge/auth-failure-reason.js +15 -8
  4. package/dist/bridge/bridge.d.ts +18 -1
  5. package/dist/bridge/bridge.js +247 -164
  6. package/dist/bridge/pairing-token.d.ts +5 -0
  7. package/dist/bridge/pairing-token.js +28 -0
  8. package/dist/bridge/protocol-skew.d.ts +37 -0
  9. package/dist/bridge/protocol-skew.js +45 -0
  10. package/dist/capsule/capsule-store.d.ts +6 -6
  11. package/dist/capsule/causal-summary.js +7 -2
  12. package/dist/cli/cli-doctor.js +31 -1
  13. package/dist/cli/cli-launch.d.ts +11 -1
  14. package/dist/cli/cli-launch.js +59 -10
  15. package/dist/cli/cli-parse.d.ts +15 -1
  16. package/dist/cli/cli-parse.js +212 -8
  17. package/dist/cli/cli-verify.d.ts +1 -0
  18. package/dist/cli/cli-verify.js +3 -2
  19. package/dist/cli/cloud-cli.js +428 -259
  20. package/dist/cli/cloud-keystore.d.ts +37 -0
  21. package/dist/cli/cloud-keystore.js +97 -0
  22. package/dist/cli/cloud-kit.d.ts +37 -0
  23. package/dist/cli/cloud-kit.js +121 -0
  24. package/dist/cli/cloud-login.d.ts +17 -0
  25. package/dist/cli/cloud-login.js +183 -0
  26. package/dist/cli/cloud-session.d.ts +48 -0
  27. package/dist/cli/cloud-session.js +53 -0
  28. package/dist/cli/config-discovery.js +1 -0
  29. package/dist/cli/daemon-start-options.d.ts +32 -0
  30. package/dist/cli/daemon-start-options.js +38 -0
  31. package/dist/cli/device-flow.d.ts +76 -0
  32. package/dist/cli/device-flow.js +56 -0
  33. package/dist/cli/doctor-rows.d.ts +9 -0
  34. package/dist/cli/doctor-rows.js +9 -0
  35. package/dist/cli/drive-attach.d.ts +13 -0
  36. package/dist/cli/drive-attach.js +18 -0
  37. package/dist/cli/drive-command.js +7 -3
  38. package/dist/cli/mcp-command.js +51 -20
  39. package/dist/cli/project-name.d.ts +30 -0
  40. package/dist/cli/project-name.js +44 -0
  41. package/dist/cli/status-next-action.d.ts +14 -0
  42. package/dist/cli/status-next-action.js +4 -4
  43. package/dist/cli.d.ts +1 -0
  44. package/dist/cli.js +134 -80
  45. package/dist/cloud/cloud-config.d.ts +22 -1
  46. package/dist/cloud/cloud-config.js +64 -5
  47. package/dist/cloud/cloud-sync.d.ts +5 -5
  48. package/dist/cloud/cloud-sync.js +33 -2
  49. package/dist/cloud/sync-cycle.d.ts +110 -0
  50. package/dist/cloud/sync-cycle.js +270 -0
  51. package/dist/cloud/sync-daemon.d.ts +69 -0
  52. package/dist/cloud/sync-daemon.js +260 -0
  53. package/dist/cloud/sync-disk.d.ts +9 -0
  54. package/dist/cloud/sync-disk.js +191 -0
  55. package/dist/cloud/sync-hash.d.ts +2 -0
  56. package/dist/cloud/sync-hash.js +35 -0
  57. package/dist/crawl/crawl-tools.js +5 -3
  58. package/dist/crawl/crawl.d.ts +3 -0
  59. package/dist/crawl/crawl.js +5 -0
  60. package/dist/daemon/daemon-resolve.d.ts +43 -0
  61. package/dist/daemon/daemon-resolve.js +69 -0
  62. package/dist/daemon/dev-servers.d.ts +15 -0
  63. package/dist/daemon/dev-servers.js +68 -0
  64. package/dist/daemon/wake-decision.d.ts +32 -0
  65. package/dist/daemon/wake-decision.js +38 -0
  66. package/dist/domain/domain-tools.js +2 -2
  67. package/dist/events/contradictions.d.ts +18 -0
  68. package/dist/events/contradictions.js +134 -21
  69. package/dist/events/declared.d.ts +7 -4
  70. package/dist/events/declared.js +15 -6
  71. package/dist/events/echo-mismatch.js +64 -14
  72. package/dist/events/event-filters.d.ts +8 -0
  73. package/dist/events/event-filters.js +19 -2
  74. package/dist/events/predicate-eval.js +53 -5
  75. package/dist/events/predicate-precheck.d.ts +8 -0
  76. package/dist/events/predicate-precheck.js +41 -0
  77. package/dist/events/predicate-route.d.ts +14 -0
  78. package/dist/events/predicate-route.js +36 -8
  79. package/dist/events/predicate.d.ts +5 -0
  80. package/dist/events/predicate.js +37 -5
  81. package/dist/events/split-text-miss.d.ts +30 -0
  82. package/dist/events/split-text-miss.js +44 -0
  83. package/dist/flows/flake.d.ts +6 -6
  84. package/dist/flows/flow-memory-consult.d.ts +55 -0
  85. package/dist/flows/flow-memory-consult.js +38 -0
  86. package/dist/flows/flow-replay-run.d.ts +18 -7
  87. package/dist/flows/flow-replay-run.js +239 -22
  88. package/dist/flows/flow-replay.js +11 -2
  89. package/dist/flows/flow-step-runners.d.ts +8 -0
  90. package/dist/flows/flow-step-runners.js +8 -2
  91. package/dist/flows/flow-store-for-session.d.ts +31 -0
  92. package/dist/flows/flow-store-for-session.js +33 -0
  93. package/dist/flows/flow-tools.js +58 -32
  94. package/dist/flows/flows.d.ts +8 -0
  95. package/dist/flows/flows.js +468 -2
  96. package/dist/flows/heal-run.js +5 -2
  97. package/dist/flows/predicate-to-expect.d.ts +17 -12
  98. package/dist/flows/predicate-to-expect.js +30 -12
  99. package/dist/flows/recordings.d.ts +13 -1
  100. package/dist/flows/recordings.js +17 -2
  101. package/dist/flows/replay.d.ts +22 -2
  102. package/dist/flows/replay.js +28 -2
  103. package/dist/flows/verify-change-tools.js +5 -2
  104. package/dist/honesty/blind-spots.d.ts +55 -2
  105. package/dist/honesty/blind-spots.js +63 -3
  106. package/dist/honesty/honesty.d.ts +24 -0
  107. package/dist/honesty/honesty.js +25 -1
  108. package/dist/honesty/instrumentation-gaps.d.ts +9 -0
  109. package/dist/honesty/instrumentation-gaps.js +29 -2
  110. package/dist/honesty/verified.d.ts +2 -1
  111. package/dist/honesty/verified.js +16 -1
  112. package/dist/http-server.js +12 -0
  113. package/dist/impact/impact-recorder.d.ts +31 -8
  114. package/dist/impact/impact-recorder.js +104 -21
  115. package/dist/impact/impact-store.d.ts +15 -1
  116. package/dist/impact/impact-store.js +43 -2
  117. package/dist/index.js +113 -7
  118. package/dist/init/agent-rules.d.ts +13 -6
  119. package/dist/init/agent-rules.js +23 -12
  120. package/dist/init/astro-patch.d.ts +16 -0
  121. package/dist/init/astro-patch.js +28 -0
  122. package/dist/init/capabilities.d.ts +4 -1
  123. package/dist/init/capabilities.js +22 -2
  124. package/dist/init/confirm.d.ts +31 -7
  125. package/dist/init/confirm.js +48 -8
  126. package/dist/init/cra.d.ts +20 -4
  127. package/dist/init/cra.js +57 -17
  128. package/dist/init/csp-check.js +17 -2
  129. package/dist/init/csp-doctor.d.ts +8 -1
  130. package/dist/init/csp-doctor.js +8 -3
  131. package/dist/init/dev-script.d.ts +3 -1
  132. package/dist/init/dev-script.js +8 -5
  133. package/dist/init/format-generated.d.ts +18 -0
  134. package/dist/init/format-generated.js +39 -0
  135. package/dist/init/install-hint.d.ts +9 -0
  136. package/dist/init/install-hint.js +29 -0
  137. package/dist/init/mcp-clients.d.ts +7 -0
  138. package/dist/init/mcp-clients.js +7 -0
  139. package/dist/init/mcp.d.ts +1 -1
  140. package/dist/init/mcp.js +1 -1
  141. package/dist/init/node-io.js +11 -1
  142. package/dist/init/plan-framework.js +81 -45
  143. package/dist/init/plan.d.ts +20 -7
  144. package/dist/init/plan.js +20 -45
  145. package/dist/init/preflight.d.ts +28 -0
  146. package/dist/init/preflight.js +38 -0
  147. package/dist/init/run.d.ts +32 -9
  148. package/dist/init/run.js +141 -119
  149. package/dist/init/slash-command.d.ts +1 -1
  150. package/dist/init/snippets.js +41 -25
  151. package/dist/init/vite-config.d.ts +1 -1
  152. package/dist/init/vite-config.js +39 -25
  153. package/dist/init/workspace-apps.d.ts +17 -10
  154. package/dist/init/workspace-apps.js +110 -0
  155. package/dist/input/network-mock-tools.js +3 -6
  156. package/dist/input/real-input.js +25 -5
  157. package/dist/input/scroll-tools.js +4 -6
  158. package/dist/input/viewport-tools.js +5 -4
  159. package/dist/intent/inline-intent.d.ts +10 -19
  160. package/dist/intent/inline-intent.js +91 -1
  161. package/dist/intent/intent-shard-store.d.ts +72 -0
  162. package/dist/intent/intent-shard-store.js +203 -0
  163. package/dist/intent/intent-shard.d.ts +411 -0
  164. package/dist/intent/intent-shard.js +176 -0
  165. package/dist/intent/intent-store.d.ts +12 -0
  166. package/dist/intent/intent-store.js +21 -0
  167. package/dist/intent/intent-subject.d.ts +45 -0
  168. package/dist/intent/intent-subject.js +177 -0
  169. package/dist/intent/intent-tools.js +65 -2
  170. package/dist/intent/open-intents.d.ts +21 -0
  171. package/dist/intent/open-intents.js +15 -0
  172. package/dist/license/license-env.d.ts +29 -0
  173. package/dist/license/license-env.js +148 -0
  174. package/dist/loopback-agent.d.ts +31 -0
  175. package/dist/loopback-agent.js +57 -0
  176. package/dist/mcp/attach-memory.d.ts +18 -2
  177. package/dist/mcp/attach-memory.js +50 -7
  178. package/dist/mcp/client-identity.d.ts +31 -0
  179. package/dist/mcp/client-identity.js +51 -0
  180. package/dist/mcp/mcp-post-transport.d.ts +26 -0
  181. package/dist/mcp/mcp-post-transport.js +109 -0
  182. package/dist/mcp/mcp-proxy.d.ts +2 -1
  183. package/dist/mcp/mcp-proxy.js +52 -62
  184. package/dist/mcp/mcp.js +7 -1
  185. package/dist/mcp/proxy-backoff.d.ts +11 -0
  186. package/dist/mcp/proxy-backoff.js +13 -0
  187. package/dist/mcp/proxy-daemon-probe.d.ts +1 -0
  188. package/dist/mcp/proxy-daemon-probe.js +7 -1
  189. package/dist/mcp/proxy-handshake.d.ts +23 -0
  190. package/dist/mcp/proxy-handshake.js +31 -0
  191. package/dist/mcp/tool-catalog-cache.d.ts +8 -0
  192. package/dist/mcp/tool-catalog-cache.js +12 -0
  193. package/dist/memory/memory-tools.d.ts +2 -0
  194. package/dist/memory/memory-tools.js +82 -0
  195. package/dist/memory/project-memory.d.ts +34 -0
  196. package/dist/memory/project-memory.js +96 -0
  197. package/dist/oracles/propose-consequences.js +12 -3
  198. package/dist/pool/browser-pool.d.ts +63 -4
  199. package/dist/pool/browser-pool.js +113 -4
  200. package/dist/pool/playwright-launcher.js +9 -0
  201. package/dist/project/project-tools.js +3 -3
  202. package/dist/project/reticle-dir.d.ts +15 -2
  203. package/dist/project/reticle-dir.js +21 -4
  204. package/dist/runs/run-store.d.ts +10 -0
  205. package/dist/runs/run-store.js +10 -0
  206. package/dist/runs/verification-sync.d.ts +0 -11
  207. package/dist/runs/verification-sync.js +22 -1
  208. package/dist/runs/verify-server.d.ts +8 -1
  209. package/dist/runs/verify-server.js +14 -2
  210. package/dist/session/command-timeout.d.ts +5 -2
  211. package/dist/session/command-timeout.js +5 -4
  212. package/dist/session/lease-visibility.d.ts +67 -0
  213. package/dist/session/lease-visibility.js +76 -0
  214. package/dist/session/no-session-diagnosis.d.ts +23 -0
  215. package/dist/session/no-session-diagnosis.js +58 -17
  216. package/dist/session/no-session-next-action.d.ts +7 -0
  217. package/dist/session/no-session-next-action.js +42 -3
  218. package/dist/session/no-session-watch.d.ts +10 -6
  219. package/dist/session/no-session-watch.js +51 -19
  220. package/dist/session/pending-commands.d.ts +11 -1
  221. package/dist/session/pending-commands.js +12 -2
  222. package/dist/session/resolve-within.d.ts +34 -0
  223. package/dist/session/resolve-within.js +53 -0
  224. package/dist/session/session-info.d.ts +18 -0
  225. package/dist/session/session-info.js +11 -0
  226. package/dist/session/session-manager.d.ts +14 -0
  227. package/dist/session/session-manager.js +61 -3
  228. package/dist/session/session-mirror.d.ts +16 -0
  229. package/dist/session/session-mirror.js +22 -0
  230. package/dist/session/session-recommendation.d.ts +19 -5
  231. package/dist/session/session-recommendation.js +25 -9
  232. package/dist/session/session-successor.js +36 -1
  233. package/dist/session/session-tools.js +2 -2
  234. package/dist/session/session.d.ts +22 -2
  235. package/dist/session/session.js +87 -7
  236. package/dist/session/wall-clock.d.ts +12 -0
  237. package/dist/session/wall-clock.js +14 -0
  238. package/dist/setup/agent-configs.d.ts +96 -0
  239. package/dist/setup/agent-configs.js +333 -0
  240. package/dist/setup/agent-io.d.ts +9 -0
  241. package/dist/setup/agent-io.js +16 -0
  242. package/dist/setup/agent-writer.d.ts +51 -0
  243. package/dist/setup/agent-writer.js +115 -0
  244. package/dist/setup/approval-migration.d.ts +47 -0
  245. package/dist/setup/approval-migration.js +68 -0
  246. package/dist/setup/auto-approve.d.ts +85 -0
  247. package/dist/setup/auto-approve.js +150 -0
  248. package/dist/setup/bridge-port.d.ts +15 -0
  249. package/dist/setup/bridge-port.js +24 -0
  250. package/dist/setup/desktop-shape.d.ts +57 -0
  251. package/dist/setup/desktop-shape.js +63 -0
  252. package/dist/setup/dev-server-wait.d.ts +68 -0
  253. package/dist/setup/dev-server-wait.js +67 -0
  254. package/dist/setup/drive-agent.d.ts +71 -0
  255. package/dist/setup/drive-agent.js +171 -0
  256. package/dist/setup/drive-plan.d.ts +68 -0
  257. package/dist/setup/drive-plan.js +130 -0
  258. package/dist/setup/ensure-daemon.d.ts +29 -0
  259. package/dist/setup/ensure-daemon.js +58 -0
  260. package/dist/setup/init-runtime.d.ts +47 -0
  261. package/dist/setup/init-runtime.js +160 -0
  262. package/dist/setup/interrupt.d.ts +25 -0
  263. package/dist/setup/interrupt.js +40 -0
  264. package/dist/setup/license-key.d.ts +39 -0
  265. package/dist/setup/license-key.js +72 -0
  266. package/dist/setup/listeners.d.ts +37 -0
  267. package/dist/setup/listeners.js +85 -0
  268. package/dist/setup/node-effects.d.ts +60 -0
  269. package/dist/setup/node-effects.js +210 -0
  270. package/dist/setup/page-probe.d.ts +39 -0
  271. package/dist/setup/page-probe.js +49 -0
  272. package/dist/setup/relaunch.d.ts +36 -0
  273. package/dist/setup/relaunch.js +57 -0
  274. package/dist/setup/remaining-steps.d.ts +19 -0
  275. package/dist/setup/remaining-steps.js +31 -0
  276. package/dist/setup/run-setup.d.ts +99 -0
  277. package/dist/setup/run-setup.js +203 -0
  278. package/dist/setup/session-pick.d.ts +47 -0
  279. package/dist/setup/session-pick.js +51 -0
  280. package/dist/setup/setup-command.d.ts +45 -0
  281. package/dist/setup/setup-command.js +199 -0
  282. package/dist/setup/setup-options.d.ts +74 -0
  283. package/dist/setup/setup-options.js +67 -0
  284. package/dist/setup/transcripts.d.ts +16 -0
  285. package/dist/setup/transcripts.js +43 -0
  286. package/dist/status-payload.d.ts +14 -1
  287. package/dist/status-payload.js +23 -1
  288. package/dist/telemetry/daemon-telemetry.d.ts +8 -1
  289. package/dist/telemetry/daemon-telemetry.js +52 -5
  290. package/dist/telemetry/error-class.js +1 -1
  291. package/dist/telemetry/feedback-context.d.ts +9 -1
  292. package/dist/telemetry/feedback-context.js +60 -29
  293. package/dist/telemetry/feedback-local.d.ts +15 -0
  294. package/dist/telemetry/feedback-local.js +56 -0
  295. package/dist/telemetry/feedback.d.ts +11 -0
  296. package/dist/telemetry/feedback.js +19 -6
  297. package/dist/telemetry/init-telemetry.d.ts +2 -0
  298. package/dist/telemetry/init-telemetry.js +2 -0
  299. package/dist/telemetry/install-source.d.ts +14 -0
  300. package/dist/telemetry/install-source.js +30 -0
  301. package/dist/telemetry/license-activation.d.ts +7 -1
  302. package/dist/telemetry/license-activation.js +9 -5
  303. package/dist/telemetry/mcp-connection.js +7 -0
  304. package/dist/telemetry/project-profile.d.ts +12 -1
  305. package/dist/telemetry/project-profile.js +11 -6
  306. package/dist/telemetry/proxy-telemetry.d.ts +3 -0
  307. package/dist/telemetry/proxy-telemetry.js +21 -0
  308. package/dist/telemetry/session-metrics.d.ts +12 -0
  309. package/dist/telemetry/session-metrics.js +35 -1
  310. package/dist/telemetry/telemetry.js +17 -2
  311. package/dist/telemetry/tool-refused.d.ts +3 -1
  312. package/dist/telemetry/tool-refused.js +34 -2
  313. package/dist/tools/act-capsule.d.ts +10 -0
  314. package/dist/tools/act-capsule.js +8 -2
  315. package/dist/tools/act-danger.d.ts +2 -1
  316. package/dist/tools/act-danger.js +11 -3
  317. package/dist/tools/act-observation.d.ts +41 -0
  318. package/dist/tools/act-observation.js +36 -0
  319. package/dist/tools/act-preflight.d.ts +20 -0
  320. package/dist/tools/act-preflight.js +98 -0
  321. package/dist/tools/act-sequence-retry.d.ts +45 -0
  322. package/dist/tools/act-sequence-retry.js +58 -0
  323. package/dist/tools/act-sequence-tool.d.ts +13 -0
  324. package/dist/tools/act-sequence-tool.js +135 -0
  325. package/dist/tools/act-target.d.ts +14 -0
  326. package/dist/tools/act-target.js +47 -0
  327. package/dist/tools/act-tools.d.ts +21 -1
  328. package/dist/tools/act-tools.js +100 -213
  329. package/dist/tools/assert-verdict.js +22 -16
  330. package/dist/tools/browser-tools.js +6 -2
  331. package/dist/tools/dynamic-tools.js +24 -5
  332. package/dist/tools/error-recovery.d.ts +1 -0
  333. package/dist/tools/error-recovery.js +8 -0
  334. package/dist/tools/invoke-tool.js +59 -9
  335. package/dist/tools/lease-tools.d.ts +30 -4
  336. package/dist/tools/lease-tools.js +169 -5
  337. package/dist/tools/merged-name-redirect.d.ts +13 -0
  338. package/dist/tools/merged-name-redirect.js +22 -0
  339. package/dist/tools/numeric-bounds.d.ts +100 -0
  340. package/dist/tools/numeric-bounds.js +125 -0
  341. package/dist/tools/observe-tools.js +47 -40
  342. package/dist/tools/read-tools.js +28 -7
  343. package/dist/tools/real-input-attempt.d.ts +10 -0
  344. package/dist/tools/real-input-attempt.js +59 -1
  345. package/dist/tools/reconcile-tools.js +4 -4
  346. package/dist/tools/tool-kit.d.ts +12 -0
  347. package/dist/tools/tool-kit.js +6 -1
  348. package/dist/tools/tool-names.d.ts +2 -0
  349. package/dist/tools/tool-names.js +2 -0
  350. package/dist/tools/tools-helpers.d.ts +19 -0
  351. package/dist/tools/tools-helpers.js +46 -0
  352. package/dist/tools/tools.d.ts +2 -0
  353. package/dist/tools/tools.js +56 -8
  354. package/dist/update/reticle-deps.d.ts +9 -0
  355. package/dist/update/reticle-deps.js +27 -0
  356. package/dist/update/sdk-sync.d.ts +1 -2
  357. package/dist/update/sdk-sync.js +1 -21
  358. package/dist/update/update-nudge.d.ts +18 -0
  359. package/dist/update/update-nudge.js +20 -0
  360. package/dist/version/peer-announce.js +4 -2
  361. package/dist/version/sdk-fix.d.ts +47 -0
  362. package/dist/version/sdk-fix.js +127 -0
  363. package/dist/version/version-nudge.d.ts +14 -0
  364. package/dist/version/version-nudge.js +21 -0
  365. package/dist/version/version-skew.d.ts +38 -3
  366. package/dist/version/version-skew.js +83 -7
  367. package/dist/visual/visual-store.d.ts +6 -6
  368. package/dist/visual/visual-store.js +18 -16
  369. package/dist/visual/visual-tools.js +57 -12
  370. package/docs/cli/doctor.mdx +1 -0
  371. package/docs/cli/serve.mdx +4 -2
  372. package/docs/cli.mdx +7 -3
  373. package/docs/desktop.mdx +1 -1
  374. package/docs/docs.json +1 -1
  375. package/docs/getting-started.md +19 -3
  376. package/docs/index.mdx +9 -6
  377. package/docs/install-agentic.mdx +38 -12
  378. package/docs/install-manual.mdx +1 -1
  379. package/docs/packages.mdx +1 -1
  380. package/docs/skill-file.mdx +3 -3
  381. package/docs/telemetry-contract.md +108 -4
  382. package/docs/tools-act-sequence.mdx +16 -0
  383. package/docs/usage.md +1 -1
  384. package/package.json +4 -3
  385. package/dist/init/cursor.d.ts +0 -24
  386. package/dist/init/cursor.js +0 -80
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
- import { EventType, ReticleCommand, SnapshotMode } from '@reticlehq/core';
2
+ import { EventType, ReticleCommand, SnapshotMode, urlForMatch } from '@reticlehq/core';
3
3
  import { ReticleTool } from './tool-names.js';
4
+ import { cursorSchema } from './numeric-bounds.js';
4
5
  import { reconcile } from '../events/reconcile.js';
5
6
  import { salvageJson } from '../events/json-salvage.js';
6
7
  import { withControl } from '../session/control-envelope.js';
@@ -29,7 +30,7 @@ async function capturedBodies(session, since, urlContains) {
29
30
  for (const event of events) {
30
31
  if (event.type !== EventType.NET_REQUEST)
31
32
  continue;
32
- const url = 'string' === typeof event.data['url'] ? event.data['url'] : '';
33
+ const url = urlForMatch(event.data);
33
34
  if (urlContains !== undefined && !url.includes(urlContains))
34
35
  continue;
35
36
  total += 1;
@@ -55,8 +56,7 @@ export const RECONCILE_TOOLS = [
55
56
  description: 'Compare what the API returned against what the page RENDERS. Catches the class no status code and no assertion can: a USD amount shown with a ₹ sign, a record the API calls `on_hold` displayed as "pending". Needs response bodies — enable `captureNetworkBodies` — and says so when they are missing rather than reporting a clean result over data it never read.',
56
57
  inputSchema: {
57
58
  ...sessionIdShape,
58
- since: z
59
- .number()
59
+ since: cursorSchema
60
60
  .optional()
61
61
  .describe('Cursor from a prior act — scopes to responses received after it. Default: 0.'),
62
62
  urlContains: z
@@ -56,6 +56,18 @@ export interface ToolDeps<Ext = unknown> {
56
56
  * exactly today's behaviour.
57
57
  */
58
58
  artifactRootFor?: (projectId: string | undefined) => ArtifactRoot;
59
+ /**
60
+ * A verification run just landed — wake cloud sync instead of waiting for its next tick.
61
+ *
62
+ * The run itself is pushed immediately on this path, but everything AROUND it — the impact
63
+ * counters, the flows it exercised, flake and intent — moves only on the timer. That is what
64
+ * produced a dashboard showing a fresh run beside stale numbers, which reads as a sync bug and is
65
+ * really just two different clocks. Waking the loop here makes the run and its context arrive
66
+ * together.
67
+ *
68
+ * Optional: absent ⇒ the timer alone, which is exactly today's behaviour.
69
+ */
70
+ onRunPersisted?: () => void;
59
71
  /**
60
72
  * This daemon's OWN project id, derived from the directory it was started in.
61
73
  *
@@ -21,7 +21,12 @@ import { normalizeLines } from '../project/baselines.js';
21
21
  export const intentArg = z
22
22
  .string()
23
23
  .optional()
24
- .describe('What this change is meant to make true, in your own words, or the id of an intent you already declared. ' +
24
+ .describe('What this change is meant to make true — as a durable statement ABOUT THE PRODUCT, or the id of an intent you already declared. ' +
25
+ 'Write it so a teammate who was not here understands it in six months: name the behaviour, not this run. ' +
26
+ 'GOOD: "a filtered issue queue is a shareable link — the filter is in the URL". ' +
27
+ 'BAD: "P1 step 5", "drive 2 of 2", "same as before the fix", "the board renders cleanly" — these name a session, ' +
28
+ 'a step number or nothing checkable, and the person reading them later has none of that context. ' +
29
+ 'This is shared memory: it is pooled per project and later agents read it back to avoid re-deriving what you just established. ' +
25
30
  'Recorded in .reticle/intent.json, the same ledger reticle_intent writes, and marked proved by this verdict if it passes.');
26
31
  export const sessionIdShape = {
27
32
  sessionId: z
@@ -53,6 +53,8 @@ export declare const ReticleTool: {
53
53
  readonly VERIFY: "reticle_verify";
54
54
  /** read cross-run history (.reticle/project.json) + diff-vs-last for a name. */
55
55
  readonly PROJECT: "reticle_project";
56
+ /** The team's shared memory, read on demand — see memory/memory-tools.ts. */
57
+ readonly MEMORY: "reticle_memory";
56
58
  /** explicitly record a run outcome (the manual companion to auto-recording). */
57
59
  readonly RUN_RECORD: "reticle_run_record";
58
60
  /** capture a pixel screenshot (CDP/driven browser) →.reticle/visual/<name>.png. */
@@ -54,6 +54,8 @@ export const ReticleTool = {
54
54
  VERIFY: 'reticle_verify',
55
55
  /** read cross-run history (.reticle/project.json) + diff-vs-last for a name. */
56
56
  PROJECT: 'reticle_project',
57
+ /** The team's shared memory, read on demand — see memory/memory-tools.ts. */
58
+ MEMORY: 'reticle_memory',
57
59
  /** explicitly record a run outcome (the manual companion to auto-recording). */
58
60
  RUN_RECORD: 'reticle_run_record',
59
61
  /** capture a pixel screenshot (CDP/driven browser) →.reticle/visual/<name>.png. */
@@ -8,6 +8,25 @@ export declare function parseInteractive(tree: string): InteractiveItem[];
8
8
  export declare function asString(value: unknown): string | undefined;
9
9
  export declare function asNumber(value: unknown): number | undefined;
10
10
  export declare function asRecord(value: unknown): Record<string, unknown>;
11
+ /**
12
+ * The session this call is aimed at: a top-level `sessionId`, or the same key on sequence steps.
13
+ *
14
+ * `reticle_act_sequence` describes each step as equivalent to one `reticle_act`, so an agent that
15
+ * learned the escape hatch on the single-action tool puts `sessionId` on the step. Reading only the
16
+ * top-level key then auto-selects a different tab, the step refs miss, and the refusal reads as a
17
+ * stale ref — a confident diagnosis of the wrong thing. Top-level still wins when both are given.
18
+ *
19
+ * Mixed step ids refuse rather than pick: two named tabs is a guess we will not make.
20
+ */
21
+ export declare function sessionIdFromArgs(args: Record<string, unknown>): string | undefined;
22
+ /**
23
+ * The ref this call is about to spend: a top-level `ref`, or the first sequence step's.
24
+ *
25
+ * Wrong-tab refusal keys off this. A sequence that only carries refs inside `steps` used to look
26
+ * like a call with no ref, so auto-selection could pick a different tab and the miss was blamed
27
+ * on the DOM.
28
+ */
29
+ export declare function spentRefFromArgs(args: Record<string, unknown>): string | undefined;
11
30
  /**
12
31
  * A `{ file, line }` source location off an untrusted result payload, or undefined.
13
32
  *
@@ -19,6 +19,52 @@ export function asNumber(value) {
19
19
  export function asRecord(value) {
20
20
  return 'object' === typeof value && value !== null ? value : {};
21
21
  }
22
+ /**
23
+ * The session this call is aimed at: a top-level `sessionId`, or the same key on sequence steps.
24
+ *
25
+ * `reticle_act_sequence` describes each step as equivalent to one `reticle_act`, so an agent that
26
+ * learned the escape hatch on the single-action tool puts `sessionId` on the step. Reading only the
27
+ * top-level key then auto-selects a different tab, the step refs miss, and the refusal reads as a
28
+ * stale ref — a confident diagnosis of the wrong thing. Top-level still wins when both are given.
29
+ *
30
+ * Mixed step ids refuse rather than pick: two named tabs is a guess we will not make.
31
+ */
32
+ export function sessionIdFromArgs(args) {
33
+ const top = asString(args['sessionId']);
34
+ if (top !== undefined)
35
+ return top;
36
+ const steps = args['steps'];
37
+ if (!Array.isArray(steps))
38
+ return undefined;
39
+ let found;
40
+ for (const raw of steps) {
41
+ const id = asString(asRecord(raw)['sessionId']);
42
+ if (id === undefined)
43
+ continue;
44
+ if (found !== undefined && found !== id) {
45
+ throw new Error(`reticle_act_sequence steps name different sessionIds ('${found}' and '${id}'). ` +
46
+ 'Pass one sessionId at the top level to target a tab. Nothing was acted on.');
47
+ }
48
+ found = id;
49
+ }
50
+ return found;
51
+ }
52
+ /**
53
+ * The ref this call is about to spend: a top-level `ref`, or the first sequence step's.
54
+ *
55
+ * Wrong-tab refusal keys off this. A sequence that only carries refs inside `steps` used to look
56
+ * like a call with no ref, so auto-selection could pick a different tab and the miss was blamed
57
+ * on the DOM.
58
+ */
59
+ export function spentRefFromArgs(args) {
60
+ const top = asString(args['ref']);
61
+ if (top !== undefined)
62
+ return top;
63
+ const steps = args['steps'];
64
+ if (!Array.isArray(steps) || 0 === steps.length)
65
+ return undefined;
66
+ return asString(asRecord(steps[0])['ref']);
67
+ }
22
68
  /**
23
69
  * A `{ file, line }` source location off an untrusted result payload, or undefined.
24
70
  *
@@ -1,6 +1,8 @@
1
1
  import { type ToolDef } from './tool-kit.js';
2
2
  import { type MergePlan } from './merge-tools.js';
3
3
  export type { ToolDef, ToolDeps } from './tool-kit.js';
4
+ /** Every handler, including tools retired from the advertised MCP surface. */
5
+ export declare const RAW_TOOLS: ToolDef[];
4
6
  /**
5
7
  * surface consolidation. Tool defs are re-sent EVERY turn, so the advertised count is a per-turn
6
8
  * tax multiplied by loop length. Sibling families collapse into one action-dispatched tool; the member
@@ -3,7 +3,8 @@ import { NoSessionAction, QueryBy, ReticleCommand, SnapshotMode } from '@reticle
3
3
  import { ReticleTool } from './tool-names.js';
4
4
  import { withSizeCost } from '../session/output-budget.js';
5
5
  import { applySnapshotDelta, SnapshotCache } from './snapshot-delta.js';
6
- import { asString, asNumber } from './tools-helpers.js';
6
+ import { asRecord, asString, asNumber } from './tools-helpers.js';
7
+ import { countSchema } from './numeric-bounds.js';
7
8
  import { normalizeQueryArgs } from './query-shape.js';
8
9
  import { paginateQueryResult } from './query-paginate.js';
9
10
  /**
@@ -24,6 +25,7 @@ import { FLOW_TOOLS } from '../flows/flow-tools.js';
24
25
  import { INTENT_TOOLS } from '../intent/intent-tools.js';
25
26
  import { CONTEXT_TOOLS } from '../runs/context-tools.js';
26
27
  import { PROJECT_TOOLS } from '../project/project-tools.js';
28
+ import { MEMORY_TOOLS } from '../memory/memory-tools.js';
27
29
  import { RUN_TOOLS } from '../runs/run-tools.js';
28
30
  import { VISUAL_TOOLS } from '../visual/visual-tools.js';
29
31
  import { AFFECTED_TOOLS } from '../flows/affected-tools.js';
@@ -46,7 +48,8 @@ import { LEASE_TOOLS } from './lease-tools.js';
46
48
  import { FEEDBACK_TOOLS } from './feedback-tools.js';
47
49
  /** Per-server last-snapshot cache backing reticle_snapshot's diff:true delta mode (route-invalidated). */
48
50
  const SNAPSHOT_CACHE = new SnapshotCache();
49
- const RAW_TOOLS = [
51
+ /** Every handler, including tools retired from the advertised MCP surface. */
52
+ export const RAW_TOOLS = [
50
53
  {
51
54
  name: ReticleTool.SESSIONS,
52
55
  description: 'List connected browser sessions (tab url/title, sessionId, last-seen, health: hidden/focused/throttled, and `realInputAvailable` — true when native CDP/launched real input is driving this tab), plus a `recommendation` naming the in-protocol escape hatch (`reticle_run { tool: "reticle_lease" }`, with `reticle drive` as the human-side equivalent) when a tab is hidden/throttled and may be un-scriptable from here.',
@@ -63,6 +66,10 @@ const RAW_TOOLS = [
63
66
  title: z.string().optional(),
64
67
  adapters: z.array(z.string()),
65
68
  hasCapabilities: z.boolean(),
69
+ runtime: z
70
+ .string()
71
+ .optional()
72
+ .describe("Which shell answered: web, electron or tauri. Absent on an SDK too old to report one — never defaulted, because a browser tab and a desktop window on the same url are otherwise indistinguishable and only one of them has the app's IPC."),
66
73
  versionSkew: z
67
74
  .string()
68
75
  .optional()
@@ -75,6 +82,11 @@ const RAW_TOOLS = [
75
82
  leased: z.boolean().optional(),
76
83
  stale: z.boolean().optional(),
77
84
  cleanup_suggestion: z.string().optional(),
85
+ unresponsive: z
86
+ .literal(true)
87
+ .optional()
88
+ .describe('Present only when this tab is attached but has stopped answering commands: every call against it will time out. The other health fields still look fine, which is what makes this state invisible without the flag.'),
89
+ unresponsive_suggestion: z.string().optional(),
78
90
  pendingMarks: z.number().optional(),
79
91
  review_suggestion: z.string().optional(),
80
92
  recommendation: z.string().optional(),
@@ -104,7 +116,7 @@ const RAW_TOOLS = [
104
116
  reason: z.string(),
105
117
  })
106
118
  .optional()
107
- .describe("Present ONLY when `sessions` is empty: the same answer as `why`, executable. `command` is the LITERAL command to run, sourced from this project's own package.json scripts and lockfile — it is absent, never guessed, when the project declares no dev script. `action` is one of start_dev_server | run_init | open_app | reopen_app."),
119
+ .describe("Present ONLY when `sessions` is empty: the same answer as `why`, executable. `command` is the LITERAL command to run, sourced from this project's own package.json scripts and lockfile — it is absent, never guessed, when the project declares no dev script. `action` is one of daemon_split | start_dev_server | run_init | open_app | reopen_app. `daemon_split` outranks the rest and means the app IS running and instrumented, on a DIFFERENT daemon than the one you are attached to — do not start or re-init anything, read `reason`."),
108
120
  },
109
121
  handler: async (deps) => {
110
122
  const provider = deps.realInput;
@@ -221,12 +233,12 @@ const RAW_TOOLS = [
221
233
  return commandOrThrow(deps, resolved.id, ReticleCommand.SNAPSHOT, {
222
234
  scope: args['scope'],
223
235
  mode,
224
- }).then((raw) => withSizeCost(applySnapshotDelta(raw, {
236
+ }).then((raw) => withSizeCost(noteEmptyLeanTree(applySnapshotDelta(raw, {
225
237
  sessionId: resolved.id,
226
238
  scope: asString(args['scope']) ?? '',
227
239
  mode,
228
240
  diff: true === args['diff'],
229
- }, SNAPSHOT_CACHE)));
241
+ }, SNAPSHOT_CACHE), mode)));
230
242
  },
231
243
  },
232
244
  {
@@ -259,9 +271,7 @@ const RAW_TOOLS = [
259
271
  .array(z.string())
260
272
  .optional()
261
273
  .describe("Attribute names to return per match, e.g. ['href'] to inventory links or ['src'] for images. Absent attributes are omitted; credential-bearing names are redacted."),
262
- limit: z
263
- .number()
264
- .nonnegative()
274
+ limit: countSchema
265
275
  .optional()
266
276
  .describe('Cap the returned descriptors to the first N (cuts tokens on broad queries). If more matched, the result carries total + truncated:true so the trim is never silent — narrow with name/scope.'),
267
277
  count_only: z
@@ -336,6 +346,15 @@ const RAW_TOOLS = [
336
346
  .describe('Structural clusters on the page — what IS here, to diagnose the miss.'),
337
347
  presentTestids: z.array(z.string()).optional(),
338
348
  knownEmptyState: z.boolean(),
349
+ splitText: z
350
+ .object({
351
+ ref: z.string().optional(),
352
+ role: z.string().optional(),
353
+ name: z.string().optional(),
354
+ })
355
+ .passthrough()
356
+ .optional()
357
+ .describe("Present when a TEXT search missed but the string IS on the page, split across this container's children. Retry as { scope: <ref>, self: true } — no text query can match a string no single element owns."),
339
358
  })
340
359
  .optional()
341
360
  .describe('Present only on zero matches — tells you what IS on the page so you can diagnose the miss.'),
@@ -467,6 +486,7 @@ const RAW_TOOLS = [
467
486
  ...FLOW_TOOLS,
468
487
  // reticle_project (read history + diff-vs-last) / reticle_run_record. See project-tools.ts.
469
488
  ...PROJECT_TOOLS,
489
+ ...MEMORY_TOOLS,
470
490
  // reticle_run_export — export the verification-run verdict artifact (.reticle/runs/). See run-tools.ts.
471
491
  ...RUN_TOOLS,
472
492
  // reticle_screenshot / reticle_visual_diff — opt-in, CDP-driven. See visual-tools.ts.
@@ -579,4 +599,32 @@ export const RETIRED_FROM_SURFACE = [
579
599
  ReticleTool.REFRESH, // absorbed into reticle_navigate { reload: true }
580
600
  ReticleTool.WAIT_READY, // server-internal: the first live call already blocks for the session
581
601
  ];
602
+ /**
603
+ * An empty `interactive` tree is a claim, and it is usually the wrong one.
604
+ *
605
+ * `mode:"interactive"` filters on ARIA role, and a large share of production UI carries none. On
606
+ * MarkText — a shipped Electron editor — it returns NOTHING while `mode:"full"` returns 47 nodes,
607
+ * because its whole block picker is `<div>`s with no role, no testid and no pointer cursor. This
608
+ * tool's own description recommends the lean mode, so an agent takes the cheap look, is handed `""`,
609
+ * and reads it as an empty page. It is the one shape of answer this product must never invent.
610
+ *
611
+ * The browser counts what leanness passed over, so the tool can say which of the two it is.
612
+ */
613
+ function noteEmptyLeanTree(result, mode) {
614
+ if (SnapshotMode.INTERACTIVE !== mode)
615
+ return result;
616
+ const row = asRecord(result);
617
+ if (0 !== asNumber(row['nodes']))
618
+ return result;
619
+ const skipped = asNumber(row['leanSkipped']) ?? 0;
620
+ if (0 === skipped)
621
+ return result;
622
+ return {
623
+ ...row,
624
+ note: `no element on this page carries an interactive ARIA role, so this mode found nothing — ` +
625
+ `${String(skipped)} element(s) were passed over for leanness and this is NOT an empty page. ` +
626
+ `Take reticle_snapshot { mode: "full" } instead; the controls here are addressed by text or ` +
627
+ `by testid rather than by role.`,
628
+ };
629
+ }
582
630
  export const TOOLS = applyMerges(RAW_TOOLS, MERGE_PLANS, RETIRED_FROM_SURFACE);
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The `@reticlehq/*` packages a project actually declares.
3
+ *
4
+ * Shared by `reticle update` (which syncs them) and the version-skew remedy (which names them).
5
+ * Lives here, not in `init/`, so the daemon library path can read a package.json without pulling
6
+ * the installer in behind it.
7
+ */
8
+ /** The Reticle packages this project declares, from either dependency section. */
9
+ export declare function reticleDepsOf(pkgJson: unknown): string[];
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The `@reticlehq/*` packages a project actually declares.
3
+ *
4
+ * Shared by `reticle update` (which syncs them) and the version-skew remedy (which names them).
5
+ * Lives here, not in `init/`, so the daemon library path can read a package.json without pulling
6
+ * the installer in behind it.
7
+ */
8
+ /** Everything we publish is scoped here. */
9
+ const RETICLE_SCOPE = '@reticlehq/';
10
+ /**
11
+ * The CLI. Never an app dependency: it carries the Node MCP server and `ws`, and putting it in a
12
+ * browser app's tree is what the retired `@reticlehq/core` umbrella got wrong.
13
+ */
14
+ const SERVER_PACKAGE = '@reticlehq/server';
15
+ function namesIn(section) {
16
+ if ('object' !== typeof section || null === section)
17
+ return [];
18
+ return Object.keys(section);
19
+ }
20
+ /** The Reticle packages this project declares, from either dependency section. */
21
+ export function reticleDepsOf(pkgJson) {
22
+ if ('object' !== typeof pkgJson || null === pkgJson)
23
+ return [];
24
+ const manifest = pkgJson;
25
+ const declared = [...namesIn(manifest['dependencies']), ...namesIn(manifest['devDependencies'])];
26
+ return [...new Set(declared)].filter((name) => name.startsWith(RETICLE_SCOPE) && name !== SERVER_PACKAGE);
27
+ }
@@ -12,8 +12,7 @@
12
12
  * npm took 2.3.0 in the next project over.
13
13
  */
14
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[];
15
+ export { reticleDepsOf } from './reticle-deps.js';
17
16
  /** The install to run, or null when this project has nothing of ours to sync. */
18
17
  export declare function sdkSyncCommand(pm: PackageManager, packages: readonly string[], version: string): {
19
18
  command: string;
@@ -12,27 +12,7 @@
12
12
  * npm took 2.3.0 in the next project over.
13
13
  */
14
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
- }
15
+ export { reticleDepsOf } from './reticle-deps.js';
36
16
  /** The install to run, or null when this project has nothing of ours to sync. */
37
17
  export function sdkSyncCommand(pm, packages, version) {
38
18
  if (0 === packages.length)
@@ -69,6 +69,24 @@ export declare function takeUpdateNudge(): UpdateNudge | undefined;
69
69
  * audiences and must not steal each other's message.
70
70
  */
71
71
  export declare function availableUpdate(currentVersion?: string): string | undefined;
72
+ /**
73
+ * What the nudge did this daemon run, for the session summary.
74
+ *
75
+ * The nudge has shipped for several releases and emitted nothing, so "did the agent get told about
76
+ * a release, and did anything happen" was unanswerable — and it is the whole adoption mechanism for
77
+ * a published fix. `versionChange.nudged` is the half that only ever arrives from machines that DID
78
+ * update; the pinned cohort never fires `version_changed` at all.
79
+ *
80
+ * Reads the same module state the delivery path uses rather than adding a counter beside it, so the
81
+ * two cannot disagree. `shown` is the one-shot delivery flag: it means "an agent was told", never
82
+ * how often. `offered` is present whenever a newer release was known, whether or not it was
83
+ * delivered — without it, `shown: false` would mean "nothing was available" and "something was and
84
+ * the nudge did not fire" at the same time, and only one of those is a defect.
85
+ */
86
+ export declare function updateNudgeState(): {
87
+ shown: boolean;
88
+ offered?: string;
89
+ };
72
90
  /** Tests only — drop the module state so each case starts clean. */
73
91
  export declare function resetUpdateNudge(): void;
74
92
  export {};
@@ -187,6 +187,26 @@ export function availableUpdate(currentVersion = SERVER_VERSION) {
187
187
  const latest = loadManifest()?.latestVersion;
188
188
  return latest !== undefined && isNewerVersion(latest, currentVersion) ? latest : undefined;
189
189
  }
190
+ /**
191
+ * What the nudge did this daemon run, for the session summary.
192
+ *
193
+ * The nudge has shipped for several releases and emitted nothing, so "did the agent get told about
194
+ * a release, and did anything happen" was unanswerable — and it is the whole adoption mechanism for
195
+ * a published fix. `versionChange.nudged` is the half that only ever arrives from machines that DID
196
+ * update; the pinned cohort never fires `version_changed` at all.
197
+ *
198
+ * Reads the same module state the delivery path uses rather than adding a counter beside it, so the
199
+ * two cannot disagree. `shown` is the one-shot delivery flag: it means "an agent was told", never
200
+ * how often. `offered` is present whenever a newer release was known, whether or not it was
201
+ * delivered — without it, `shown: false` would mean "nothing was available" and "something was and
202
+ * the nudge did not fire" at the same time, and only one of those is a defect.
203
+ */
204
+ export function updateNudgeState() {
205
+ return {
206
+ shown: delivered,
207
+ ...(pending === undefined ? {} : { offered: pending.latestVersion }),
208
+ };
209
+ }
190
210
  /** Tests only — drop the module state so each case starts clean. */
191
211
  export function resetUpdateNudge() {
192
212
  pending = undefined;
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import { CONTRACT_FINGERPRINT } from '@reticlehq/core';
15
15
  import { SERVER_VERSION } from './server-version.js';
16
- import { describeSkew, DAEMON_FIX, SkewPair } from './version-skew.js';
16
+ import { daemonFix, describeSkew, SkewPair } from './version-skew.js';
17
17
  import { noteVersionSkew } from './version-nudge.js';
18
18
  export const PEER_VERSION_PARAM = 'peerVersion';
19
19
  export const PEER_CONTRACT_PARAM = 'peerContract';
@@ -23,7 +23,9 @@ export function noteAgentPeer(version, contract) {
23
23
  what: "the agent's MCP server",
24
24
  version: version ?? undefined,
25
25
  contract: contract ?? undefined,
26
- fix: DAEMON_FIX,
26
+ // Inverted from cli-launch: here THIS process is the daemon and the peer is the agent's
27
+ // MCP server, so the daemon is the newer half whenever the announcing agent is behind.
28
+ fix: daemonFix(SERVER_VERSION, version ?? undefined),
27
29
  }, { version: SERVER_VERSION, contract: CONTRACT_FINGERPRINT });
28
30
  if (skew !== undefined)
29
31
  noteVersionSkew(SkewPair.DAEMON, skew);
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The project-aware SDK remedy for version skew (#618).
3
+ *
4
+ * `sdkFix` in version-skew.ts is the no-project fallback: it names `@reticlehq/browser` and npm,
5
+ * because those answers are never actively wrong. When a project directory IS in hand, name the
6
+ * packages actually in package.json and the manager the lockfile implies.
7
+ *
8
+ * Does not import `init/`. The library path must never reach the installer
9
+ * (`library-path-boundary.test.ts`); `reticleDepsOf` lives in `update/` for that reason, and the
10
+ * lockfile → manager map is the same evidence `detectPackageManager` reads, kept here so a HELLO
11
+ * does not load the install plan.
12
+ */
13
+ /** What a project contributes to the remedy, when we were able to read one. */
14
+ export interface SdkFixContext {
15
+ /** `@reticlehq/*` packages declared in package.json. Empty / omitted means none yet. */
16
+ packages?: readonly string[];
17
+ packageManager: PackageManagerName;
18
+ }
19
+ /** Lockfile / marker → manager, same evidence `init/detect.ts` uses. */
20
+ export declare const PackageManagerName: {
21
+ readonly PNPM: "pnpm";
22
+ readonly YARN: "yarn";
23
+ readonly BUN: "bun";
24
+ readonly NPM: "npm";
25
+ };
26
+ export type PackageManagerName = (typeof PackageManagerName)[keyof typeof PackageManagerName];
27
+ /**
28
+ * The one sentence telling the human how to bring the page's SDK in line with this daemon.
29
+ *
30
+ * No context → the framework-neutral sensor and npm, never the React kit. Packages from
31
+ * package.json win when present. When the project has none of ours yet, the sensor is named
32
+ * rather than `@reticlehq/react` — that is the Vue/Nuxt failure.
33
+ */
34
+ export declare function resolveSdkFix(daemonVersion: string, ctx?: Partial<SdkFixContext>): string;
35
+ /**
36
+ * Read the context `resolveSdkFix` needs off a parsed manifest and the lockfiles/markers present.
37
+ *
38
+ * Undefined when there is no manifest to interpret — the caller then uses the no-project fallback.
39
+ * Pure: no filesystem.
40
+ */
41
+ export declare function sdkFixContextOf(pkgJson: unknown, lockfiles: ReadonlySet<string>, nodeModulesMarkers?: ReadonlySet<string>): SdkFixContext | undefined;
42
+ /**
43
+ * The remedy for the project in `directory`, or the no-project fallback when it cannot be read.
44
+ *
45
+ * `read` is injected so the decision is testable without a filesystem; the daemon uses the default.
46
+ */
47
+ export declare function sdkFixForDirectory(daemonVersion: string, directory: string, read?: (path: string) => string | undefined): string;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * The project-aware SDK remedy for version skew (#618).
3
+ *
4
+ * `sdkFix` in version-skew.ts is the no-project fallback: it names `@reticlehq/browser` and npm,
5
+ * because those answers are never actively wrong. When a project directory IS in hand, name the
6
+ * packages actually in package.json and the manager the lockfile implies.
7
+ *
8
+ * Does not import `init/`. The library path must never reach the installer
9
+ * (`library-path-boundary.test.ts`); `reticleDepsOf` lives in `update/` for that reason, and the
10
+ * lockfile → manager map is the same evidence `detectPackageManager` reads, kept here so a HELLO
11
+ * does not load the install plan.
12
+ */
13
+ import { join } from 'node:path';
14
+ import { readFileSync } from 'node:fs';
15
+ import { reticleDepsOf } from '../update/reticle-deps.js';
16
+ /** Lockfile / marker → manager, same evidence `init/detect.ts` uses. */
17
+ export const PackageManagerName = {
18
+ PNPM: 'pnpm',
19
+ YARN: 'yarn',
20
+ BUN: 'bun',
21
+ NPM: 'npm',
22
+ };
23
+ /** Same package a Vue/Nuxt install gets — never the React kit. */
24
+ const FRAMEWORK_NEUTRAL_SDK = '@reticlehq/browser';
25
+ const PACKAGE_JSON = 'package.json';
26
+ const NODE_MODULES = 'node_modules';
27
+ const LOCKFILE_NAMES = ['pnpm-lock.yaml', 'yarn.lock', 'bun.lockb', 'bun.lock'];
28
+ /** Markers each manager leaves inside `node_modules` — stronger than an uncommitted lockfile. */
29
+ const NODE_MODULES_MARKERS = [
30
+ ['.modules.yaml', PackageManagerName.PNPM],
31
+ ['.yarn-state.yml', PackageManagerName.YARN],
32
+ ['.package-lock.json', PackageManagerName.NPM],
33
+ ];
34
+ const INSTALL_FLAGS = {
35
+ [PackageManagerName.PNPM]: ['add', '-D'],
36
+ [PackageManagerName.YARN]: ['add', '-D'],
37
+ [PackageManagerName.BUN]: ['add', '-d'],
38
+ [PackageManagerName.NPM]: ['i', '-D'],
39
+ };
40
+ function packagesFor(ctx) {
41
+ if (ctx?.packages !== undefined && 0 !== ctx.packages.length)
42
+ return ctx.packages;
43
+ return [FRAMEWORK_NEUTRAL_SDK];
44
+ }
45
+ function managerOf(lockfiles, nodeModulesMarkers) {
46
+ if (lockfiles.has('pnpm-lock.yaml'))
47
+ return PackageManagerName.PNPM;
48
+ if (lockfiles.has('yarn.lock'))
49
+ return PackageManagerName.YARN;
50
+ if (lockfiles.has('bun.lockb') || lockfiles.has('bun.lock'))
51
+ return PackageManagerName.BUN;
52
+ for (const [name, pm] of NODE_MODULES_MARKERS) {
53
+ if (nodeModulesMarkers.has(name))
54
+ return pm;
55
+ }
56
+ return PackageManagerName.NPM;
57
+ }
58
+ function installLine(pm, pkgs) {
59
+ return `${pm} ${[...INSTALL_FLAGS[pm], ...pkgs].join(' ')}`;
60
+ }
61
+ function restartClause() {
62
+ return ('or run `reticle update`, then restart their dev server so the page reloads with it. The ' +
63
+ 'restart is not optional: a bundler keeps serving the pre-bundled copy it already has, so an ' +
64
+ 'upgrade can look applied — matching versions in `npm ls` — while the page runs the old module.');
65
+ }
66
+ /**
67
+ * The one sentence telling the human how to bring the page's SDK in line with this daemon.
68
+ *
69
+ * No context → the framework-neutral sensor and npm, never the React kit. Packages from
70
+ * package.json win when present. When the project has none of ours yet, the sensor is named
71
+ * rather than `@reticlehq/react` — that is the Vue/Nuxt failure.
72
+ */
73
+ export function resolveSdkFix(daemonVersion, ctx) {
74
+ const pm = ctx?.packageManager ?? PackageManagerName.NPM;
75
+ const pinned = packagesFor(ctx).map((name) => `${name}@${daemonVersion}`);
76
+ return `Tell the human to install the matching SDK (\`${installLine(pm, pinned)}\`) ${restartClause()}`;
77
+ }
78
+ /**
79
+ * Read the context `resolveSdkFix` needs off a parsed manifest and the lockfiles/markers present.
80
+ *
81
+ * Undefined when there is no manifest to interpret — the caller then uses the no-project fallback.
82
+ * Pure: no filesystem.
83
+ */
84
+ export function sdkFixContextOf(pkgJson, lockfiles, nodeModulesMarkers = new Set()) {
85
+ if ('object' !== typeof pkgJson || null === pkgJson)
86
+ return undefined;
87
+ return {
88
+ packages: reticleDepsOf(pkgJson),
89
+ packageManager: managerOf(lockfiles, nodeModulesMarkers),
90
+ };
91
+ }
92
+ /** Reads a file, or undefined if it is not there / not readable. */
93
+ function readTextFile(path) {
94
+ try {
95
+ return readFileSync(path, 'utf8');
96
+ }
97
+ catch {
98
+ return undefined;
99
+ }
100
+ }
101
+ function parseJson(raw) {
102
+ if (raw === undefined)
103
+ return undefined;
104
+ try {
105
+ return JSON.parse(raw);
106
+ }
107
+ catch {
108
+ return undefined;
109
+ }
110
+ }
111
+ function present(directory, names, read, extra = '') {
112
+ const found = new Set();
113
+ for (const name of names) {
114
+ if (read(join(directory, extra, name)) !== undefined)
115
+ found.add(name);
116
+ }
117
+ return found;
118
+ }
119
+ /**
120
+ * The remedy for the project in `directory`, or the no-project fallback when it cannot be read.
121
+ *
122
+ * `read` is injected so the decision is testable without a filesystem; the daemon uses the default.
123
+ */
124
+ export function sdkFixForDirectory(daemonVersion, directory, read = readTextFile) {
125
+ const ctx = sdkFixContextOf(parseJson(read(join(directory, PACKAGE_JSON))), present(directory, LOCKFILE_NAMES, read), present(directory, NODE_MODULES_MARKERS.map(([name]) => name), read, NODE_MODULES));
126
+ return resolveSdkFix(daemonVersion, ctx);
127
+ }