@reticlehq/server 3.1.0 → 3.3.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 (384) hide show
  1. package/SKILL.md +30 -28
  2. package/dist/command/cli/adhoc-suite.d.ts +19 -0
  3. package/dist/command/cli/adhoc-suite.js +88 -0
  4. package/dist/command/cli/adhoc-verdict.d.ts +23 -2
  5. package/dist/command/cli/adhoc-verdict.js +18 -10
  6. package/dist/command/cli/auth/device-flow.d.ts +2 -2
  7. package/dist/command/cli/cli-doctor.js +37 -5
  8. package/dist/command/cli/cli-flow-commands.d.ts +2 -1
  9. package/dist/command/cli/cli-kill.js +1 -1
  10. package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
  11. package/dist/command/cli/cli-parse-grammar.js +7 -0
  12. package/dist/command/cli/cli-parse-verify.d.ts +2 -5
  13. package/dist/command/cli/cli-parse-verify.js +86 -13
  14. package/dist/command/cli/cli-parse.d.ts +8 -1
  15. package/dist/command/cli/cli-parse.js +38 -7
  16. package/dist/command/cli/cli-verify.d.ts +59 -0
  17. package/dist/command/cli/cli-verify.js +131 -19
  18. package/dist/command/cli/cloud-cli.js +7 -6
  19. package/dist/command/cli/cloud-kit.d.ts +0 -2
  20. package/dist/command/cli/cloud-kit.js +3 -7
  21. package/dist/command/cli/config/config-discovery.d.ts +12 -0
  22. package/dist/command/cli/config/config-discovery.js +19 -0
  23. package/dist/command/cli/doctor/doctor-rows.d.ts +12 -0
  24. package/dist/command/cli/doctor/doctor-rows.js +12 -0
  25. package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
  26. package/dist/command/cli/drive/drive-command.js +1 -1
  27. package/dist/command/cli/launch/cli-launch.d.ts +3 -23
  28. package/dist/command/cli/launch/cli-launch.js +29 -70
  29. package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +33 -0
  30. package/dist/command/cli/lifecycle/daemon-lifecycle.js +198 -0
  31. package/dist/command/cli/mcp-command.js +5 -3
  32. package/dist/command/cli/ports/resolve/cli-port.d.ts +15 -1
  33. package/dist/command/cli/ports/resolve/cli-port.js +50 -2
  34. package/dist/command/cli/report-command.d.ts +16 -0
  35. package/dist/command/cli/report-command.js +97 -0
  36. package/dist/command/cli/status/status-lines.d.ts +28 -0
  37. package/dist/command/cli/status/status-lines.js +117 -0
  38. package/dist/command/cli.d.ts +1 -1
  39. package/dist/command/cli.js +29 -177
  40. package/dist/command/daemon/binding/daemon-status-probe.d.ts +32 -0
  41. package/dist/command/daemon/binding/daemon-status-probe.js +93 -0
  42. package/dist/command/daemon/binding/loopback-reach.d.ts +42 -0
  43. package/dist/command/daemon/binding/loopback-reach.js +63 -0
  44. package/dist/command/daemon/binding/port-presence.d.ts +1 -1
  45. package/dist/command/daemon/daemon-resilience.d.ts +13 -1
  46. package/dist/command/daemon/daemon-resilience.js +12 -1
  47. package/dist/command/daemon/daemon.d.ts +2 -11
  48. package/dist/command/daemon/daemon.js +8 -11
  49. package/dist/command/daemon/idle-shutdown.d.ts +2 -1
  50. package/dist/command/daemon/lifetime/heartbeat.d.ts +8 -0
  51. package/dist/command/daemon/lifetime/heartbeat.js +10 -0
  52. package/dist/command/daemon/lifetime/log-cap.d.ts +68 -0
  53. package/dist/command/daemon/lifetime/log-cap.js +115 -0
  54. package/dist/command/setup/agent-notice-memory.d.ts +16 -0
  55. package/dist/command/setup/agent-notice-memory.js +65 -0
  56. package/dist/command/setup/bringup/dev-server-wait.d.ts +1 -0
  57. package/dist/command/setup/bringup/dev-server-wait.js +16 -0
  58. package/dist/command/setup/bringup/ensure-daemon.js +1 -1
  59. package/dist/command/setup/init/init-host.js +8 -0
  60. package/dist/command/setup/init/init-runtime.js +13 -1
  61. package/dist/command/setup/node-effects.js +1 -1
  62. package/dist/command/setup/probe/page-probe.js +9 -2
  63. package/dist/command/setup/run-setup.d.ts +25 -7
  64. package/dist/command/setup/run-setup.js +51 -9
  65. package/dist/command/setup/setup-command.js +31 -3
  66. package/dist/command/setup/terminal/confirm.js +2 -1
  67. package/dist/command/update/updater.d.ts +27 -0
  68. package/dist/command/update/updater.js +35 -2
  69. package/dist/features/crawl/crawl-tools.js +3 -1
  70. package/dist/features/crawl/crawl.d.ts +2 -0
  71. package/dist/features/crawl/crawl.js +3 -1
  72. package/dist/features/harness/drive-report.d.ts +73 -0
  73. package/dist/features/harness/drive-report.js +196 -0
  74. package/dist/features/harness/driver.js +29 -8
  75. package/dist/features/harness/drivers.d.ts +19 -0
  76. package/dist/features/harness/drivers.js +19 -0
  77. package/dist/features/harness/fill-values.d.ts +67 -0
  78. package/dist/features/harness/fill-values.js +153 -0
  79. package/dist/features/harness/jev-driver.d.ts +188 -0
  80. package/dist/features/harness/jev-driver.js +661 -0
  81. package/dist/features/harness/openai-driver.d.ts +46 -0
  82. package/dist/features/harness/openai-driver.js +197 -0
  83. package/dist/features/harness/platform-config.d.ts +62 -0
  84. package/dist/features/harness/platform-config.js +97 -0
  85. package/dist/features/visual/visual-tools.js +26 -2
  86. package/dist/index.d.ts +1 -1
  87. package/dist/index.js +80 -130
  88. package/dist/judgement/capsule/capsule-store.d.ts +31 -2
  89. package/dist/judgement/capsule/capsule-store.js +58 -6
  90. package/dist/judgement/capsule/capsule.js +5 -0
  91. package/dist/judgement/capsule/causal-summary.d.ts +13 -1
  92. package/dist/judgement/capsule/causal-summary.js +7 -0
  93. package/dist/judgement/domain/domain-model.js +38 -9
  94. package/dist/judgement/domain/domain-tools.js +4 -2
  95. package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
  96. package/dist/judgement/phenomena/phenomena.d.ts +2 -1
  97. package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
  98. package/dist/judgement/runs/artifact/gap-report.js +46 -0
  99. package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
  100. package/dist/judgement/runs/artifact/gap-summary.js +85 -0
  101. package/dist/judgement/runs/artifact/render-report.js +4 -1
  102. package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
  103. package/dist/judgement/runs/artifact/to-artifact.js +1 -1
  104. package/dist/judgement/runs/context-tools.js +21 -3
  105. package/dist/judgement/runs/drive-run.js +7 -2
  106. package/dist/judgement/runs/verification-sync.d.ts +2 -2
  107. package/dist/judgement/runs/verification-sync.js +9 -2
  108. package/dist/language/flows/affected-tools.js +6 -3
  109. package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
  110. package/dist/language/flows/annotate-notes/annotate.js +66 -18
  111. package/dist/language/flows/flow-anchor.d.ts +12 -0
  112. package/dist/language/flows/flow-anchor.js +20 -0
  113. package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
  114. package/dist/language/flows/flow-expect-grammar.js +92 -11
  115. package/dist/language/flows/flow-intent.js +1 -1
  116. package/dist/language/flows/flow-learning.d.ts +3 -0
  117. package/dist/language/flows/flow-learning.js +39 -8
  118. package/dist/language/flows/flow-replay-run.d.ts +55 -10
  119. package/dist/language/flows/flow-replay-run.js +217 -58
  120. package/dist/language/flows/flow-replay-types.d.ts +9 -0
  121. package/dist/language/flows/flow-replay.d.ts +29 -11
  122. package/dist/language/flows/flow-replay.js +221 -203
  123. package/dist/language/flows/flow-result.d.ts +18 -1
  124. package/dist/language/flows/flow-result.js +18 -2
  125. package/dist/language/flows/flow-step-runners.d.ts +26 -10
  126. package/dist/language/flows/flow-step-runners.js +20 -4
  127. package/dist/language/flows/flow-store-for-session.d.ts +2 -1
  128. package/dist/language/flows/flow-success.d.ts +21 -6
  129. package/dist/language/flows/flow-success.js +50 -138
  130. package/dist/language/flows/flow-tools.js +8 -4
  131. package/dist/language/flows/flows.d.ts +15 -10
  132. package/dist/language/flows/flows.js +17 -4
  133. package/dist/language/flows/heal.d.ts +9 -1
  134. package/dist/language/flows/heal.js +24 -2
  135. package/dist/language/flows/learned-sources.d.ts +21 -0
  136. package/dist/language/flows/learned-sources.js +84 -0
  137. package/dist/language/flows/narrow-write.d.ts +4 -4
  138. package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
  139. package/dist/language/flows/recording/recording-backtrack.js +1 -1
  140. package/dist/language/flows/recording/replay-halt.d.ts +2 -1
  141. package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
  142. package/dist/language/flows/replay.js +13 -9
  143. package/dist/language/flows/stores/annotation-store.d.ts +5 -5
  144. package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
  145. package/dist/language/flows/stores/flake-store.js +2 -3
  146. package/dist/language/flows/suite/coverage.d.ts +1 -1
  147. package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
  148. package/dist/language/flows/suite-selection.d.ts +15 -1
  149. package/dist/language/flows/suite-selection.js +1 -1
  150. package/dist/language/flows/verify-change-tools.js +21 -4
  151. package/dist/memory/cloud/account-state.d.ts +1 -1
  152. package/dist/memory/cloud/account-state.js +5 -4
  153. package/dist/memory/cloud/cloud-config.d.ts +9 -0
  154. package/dist/memory/cloud/cloud-config.js +9 -0
  155. package/dist/memory/cloud/cloud-sync.d.ts +7 -4
  156. package/dist/memory/cloud/cloud-sync.js +11 -6
  157. package/dist/memory/cloud/harness-config.d.ts +12 -0
  158. package/dist/memory/cloud/harness-config.js +58 -0
  159. package/dist/memory/cloud/harness-offer.d.ts +44 -0
  160. package/dist/memory/cloud/harness-offer.js +146 -0
  161. package/dist/memory/cloud/harness-switch.d.ts +11 -0
  162. package/dist/memory/cloud/harness-switch.js +53 -0
  163. package/dist/memory/cloud/sync-cycle.d.ts +12 -0
  164. package/dist/memory/cloud/sync-cycle.js +29 -4
  165. package/dist/memory/cloud/sync-daemon.d.ts +35 -2
  166. package/dist/memory/cloud/sync-daemon.js +45 -0
  167. package/dist/memory/cloud/sync-disk.js +11 -6
  168. package/dist/memory/impact/impact-recorder.d.ts +8 -0
  169. package/dist/memory/impact/impact-recorder.js +2 -1
  170. package/dist/memory/impact/impact-store.d.ts +5 -0
  171. package/dist/memory/impact/impact-store.js +25 -0
  172. package/dist/memory/intent/inline-intent.js +8 -3
  173. package/dist/memory/intent/intent-shard-store.d.ts +33 -47
  174. package/dist/memory/intent/intent-shard-store.js +253 -141
  175. package/dist/memory/intent/intent-shard.d.ts +22 -23
  176. package/dist/memory/intent/intent-shard.js +1 -1
  177. package/dist/memory/intent/intent-store.d.ts +13 -8
  178. package/dist/memory/intent/intent-store.js +46 -90
  179. package/dist/memory/intent/intent-tools.js +34 -23
  180. package/dist/memory/journal/ambient-store.js +2 -3
  181. package/dist/memory/journal/attach-journal.d.ts +11 -0
  182. package/dist/memory/journal/attach-journal.js +1 -1
  183. package/dist/memory/journal/deviation-report.js +4 -1
  184. package/dist/memory/journal/deviation-service.js +10 -2
  185. package/dist/memory/journal/drive-flow.d.ts +2 -2
  186. package/dist/memory/journal/drive-flow.js +21 -12
  187. package/dist/memory/journal/envelope-store.js +2 -3
  188. package/dist/memory/journal/journal-query.d.ts +35 -1
  189. package/dist/memory/journal/journal-query.js +46 -0
  190. package/dist/memory/journal/journal-recorder.d.ts +71 -2
  191. package/dist/memory/journal/journal-recorder.js +45 -7
  192. package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
  193. package/dist/memory/journal/on-disk/retain-policy.js +44 -0
  194. package/dist/memory/journal/on-disk/retention.d.ts +91 -2
  195. package/dist/memory/journal/on-disk/retention.js +181 -11
  196. package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
  197. package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
  198. package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -2
  199. package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -38
  200. package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
  201. package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
  202. package/dist/memory/journal/route-template.d.ts +22 -0
  203. package/dist/memory/journal/route-template.js +44 -0
  204. package/dist/memory/journal/self-observation.d.ts +3 -0
  205. package/dist/memory/journal/self-observation.js +67 -0
  206. package/dist/memory/journal/session-end.d.ts +17 -4
  207. package/dist/memory/journal/session-end.js +85 -7
  208. package/dist/memory/journal/session-journal.d.ts +122 -5
  209. package/dist/memory/journal/session-journal.js +489 -32
  210. package/dist/memory/project/artifact-root-resolver.d.ts +3 -0
  211. package/dist/memory/project/artifact-root-resolver.js +89 -0
  212. package/dist/memory/project/artifact-root.d.ts +39 -2
  213. package/dist/memory/project/artifact-root.js +58 -1
  214. package/dist/memory/project/baselines.d.ts +1 -1
  215. package/dist/memory/project/dir/fill-value-store.d.ts +32 -0
  216. package/dist/memory/project/dir/fill-value-store.js +88 -0
  217. package/dist/memory/project/dir/reticle-dir.d.ts +15 -5
  218. package/dist/memory/project/dir/reticle-dir.js +11 -0
  219. package/dist/memory/project/fs/fs-port.d.ts +22 -1
  220. package/dist/memory/project/fs/fs-port.js +72 -5
  221. package/dist/memory/project/fs/write-atomic.d.ts +26 -0
  222. package/dist/memory/project/fs/write-atomic.js +48 -0
  223. package/dist/memory/project/learned-routes.d.ts +16 -2
  224. package/dist/memory/project/learned-routes.js +26 -10
  225. package/dist/memory/project/project-for-root.d.ts +30 -0
  226. package/dist/memory/project/project-for-root.js +43 -0
  227. package/dist/memory/project/project-tools.js +10 -5
  228. package/dist/memory/project/session-root.d.ts +35 -6
  229. package/dist/memory/project/session-root.js +70 -8
  230. package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
  231. package/dist/memory/recall/prior/connection-memory.js +8 -0
  232. package/dist/memory/recall/registered-projects.d.ts +2 -1
  233. package/dist/portal/bridge/auth-failure-reason.d.ts +9 -0
  234. package/dist/portal/bridge/auth-failure-reason.js +20 -3
  235. package/dist/portal/bridge/bridge.d.ts +6 -1
  236. package/dist/portal/bridge/bridge.js +36 -2
  237. package/dist/portal/input/real-input.js +2 -1
  238. package/dist/portal/pool/browser-pool.d.ts +10 -0
  239. package/dist/portal/pool/browser-pool.js +10 -0
  240. package/dist/portal/pool/playwright-launcher.js +1 -0
  241. package/dist/portal/pool/pool-contract.d.ts +2 -0
  242. package/dist/portal/pool/storage-seed.d.ts +2 -1
  243. package/dist/portal/pool/zero-install.d.ts +27 -0
  244. package/dist/portal/pool/zero-install.js +54 -0
  245. package/dist/portal/realm/web-realm.js +6 -0
  246. package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
  247. package/dist/portal/session/dev-server/served-document.d.ts +21 -0
  248. package/dist/portal/session/dev-server/served-document.js +57 -0
  249. package/dist/portal/session/facts/background-traffic.d.ts +9 -0
  250. package/dist/portal/session/facts/background-traffic.js +25 -0
  251. package/dist/portal/session/facts/session-replaced.d.ts +19 -0
  252. package/dist/portal/session/facts/session-replaced.js +22 -0
  253. package/dist/portal/session/live-control-tools.js +48 -3
  254. package/dist/portal/session/no-session-diagnosis.d.ts +1 -0
  255. package/dist/portal/session/no-session-diagnosis.js +67 -60
  256. package/dist/portal/session/no-session-next-action.js +3 -2
  257. package/dist/portal/session/no-session-watch.js +14 -10
  258. package/dist/portal/session/presence/attachment-history.d.ts +13 -2
  259. package/dist/portal/session/rebind.d.ts +18 -0
  260. package/dist/portal/session/rebind.js +38 -0
  261. package/dist/portal/session/session-health.d.ts +61 -0
  262. package/dist/portal/session/session-health.js +40 -1
  263. package/dist/portal/session/session-info.d.ts +6 -0
  264. package/dist/portal/session/session-manager.d.ts +13 -0
  265. package/dist/portal/session/session-manager.js +42 -3
  266. package/dist/portal/session/session-successor.d.ts +11 -0
  267. package/dist/portal/session/session-successor.js +14 -1
  268. package/dist/portal/session/session-verdict-facts.d.ts +28 -0
  269. package/dist/portal/session/session-verdict-facts.js +15 -0
  270. package/dist/portal/session/session.d.ts +26 -10
  271. package/dist/portal/session/session.js +48 -49
  272. package/dist/status-payload.d.ts +12 -1
  273. package/dist/status-payload.js +4 -1
  274. package/dist/surface/http-server.js +3 -5
  275. package/dist/surface/mcp/faults/mcp-outage.d.ts +2 -1
  276. package/dist/surface/mcp/faults/mcp-outage.js +20 -4
  277. package/dist/surface/mcp/mcp-proxy.d.ts +10 -1
  278. package/dist/surface/mcp/mcp-proxy.js +23 -12
  279. package/dist/surface/mcp/mcp.d.ts +10 -1
  280. package/dist/surface/mcp/mcp.js +59 -29
  281. package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +15 -2
  282. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +22 -5
  283. package/dist/surface/mcp/proxy/proxy-log.d.ts +5 -11
  284. package/dist/surface/mcp/proxy/proxy-log.js +7 -22
  285. package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
  286. package/dist/surface/mcp/server-instructions.js +12 -1
  287. package/dist/surface/tools/act/act-preflight.js +6 -1
  288. package/dist/surface/tools/act/already-true.d.ts +40 -0
  289. package/dist/surface/tools/act/already-true.js +36 -0
  290. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
  291. package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
  292. package/dist/surface/tools/act/sequence-grade.js +13 -0
  293. package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
  294. package/dist/surface/tools/act/settle-in-flight.js +29 -6
  295. package/dist/surface/tools/act-capsule.js +49 -8
  296. package/dist/surface/tools/act-tools.js +25 -24
  297. package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
  298. package/dist/surface/tools/args/numeric-bounds.js +30 -3
  299. package/dist/surface/tools/assert/assert-grade.js +14 -2
  300. package/dist/surface/tools/assert/assert-verdict.js +74 -21
  301. package/dist/surface/tools/browser-tools.js +4 -3
  302. package/dist/surface/tools/coverage-tools.js +5 -2
  303. package/dist/surface/tools/dynamic-tools.js +43 -46
  304. package/dist/surface/tools/error-recovery.d.ts +6 -0
  305. package/dist/surface/tools/error-recovery.js +15 -0
  306. package/dist/surface/tools/explore-tools.js +80 -7
  307. package/dist/surface/tools/feedback-tools.d.ts +2 -1
  308. package/dist/surface/tools/feedback-tools.js +50 -3
  309. package/dist/surface/tools/first-sentence.d.ts +24 -0
  310. package/dist/surface/tools/first-sentence.js +0 -0
  311. package/dist/surface/tools/harness-explore.d.ts +125 -1
  312. package/dist/surface/tools/harness-explore.js +401 -15
  313. package/dist/surface/tools/harness-plan.d.ts +76 -0
  314. package/dist/surface/tools/harness-plan.js +156 -0
  315. package/dist/surface/tools/harness-toolset.js +12 -1
  316. package/dist/surface/tools/invoke-tool.js +82 -9
  317. package/dist/surface/tools/lease-hint.d.ts +8 -0
  318. package/dist/surface/tools/lease-hint.js +10 -0
  319. package/dist/surface/tools/lease-tools.d.ts +16 -28
  320. package/dist/surface/tools/lease-tools.js +53 -5
  321. package/dist/surface/tools/lineage-tools.js +1 -1
  322. package/dist/surface/tools/live-call-text.d.ts +6 -0
  323. package/dist/surface/tools/live-call-text.js +36 -2
  324. package/dist/surface/tools/observe-tools.js +69 -15
  325. package/dist/surface/tools/read/query-windowed.d.ts +1 -1
  326. package/dist/surface/tools/read/query-windowed.js +1 -1
  327. package/dist/surface/tools/read-tools.js +1 -2
  328. package/dist/surface/tools/real-input-attempt.d.ts +0 -5
  329. package/dist/surface/tools/real-input-attempt.js +1 -1
  330. package/dist/surface/tools/tool-kit.d.ts +23 -3
  331. package/dist/surface/tools/tool-kit.js +6 -1
  332. package/dist/surface/tools/tool-surface.js +17 -5
  333. package/dist/surface/tools/tools-helpers.d.ts +8 -7
  334. package/dist/surface/tools/tools-helpers.js +11 -13
  335. package/dist/surface/tools/tools.js +70 -30
  336. package/dist/telemetry/bug-found.js +32 -2
  337. package/dist/telemetry/feedback-context.js +3 -3
  338. package/dist/telemetry/feedback-local.d.ts +1 -0
  339. package/dist/telemetry/feedback-local.js +1 -3
  340. package/dist/telemetry/feedback.d.ts +1 -1
  341. package/dist/telemetry/harness-drive.d.ts +6 -0
  342. package/dist/telemetry/harness-drive.js +39 -0
  343. package/dist/telemetry/hud-metrics.d.ts +32 -0
  344. package/dist/telemetry/hud-metrics.js +95 -0
  345. package/dist/telemetry/mcp-connection.d.ts +14 -1
  346. package/dist/telemetry/mcp-connection.js +17 -2
  347. package/dist/telemetry/onboarding-firsts.d.ts +30 -1
  348. package/dist/telemetry/onboarding-firsts.js +54 -12
  349. package/dist/telemetry/session-metrics.d.ts +5 -0
  350. package/dist/telemetry/session-metrics.js +15 -1
  351. package/dist/telemetry/verification-of.js +31 -3
  352. package/dist/wire-journal.d.ts +31 -0
  353. package/dist/wire-journal.js +100 -0
  354. package/docs/README.md +1 -0
  355. package/docs/agent-cheatsheet.md +7 -3
  356. package/docs/autodrive.md +42 -2
  357. package/docs/benchmarks.md +1 -1
  358. package/docs/capabilities.mdx +1 -1
  359. package/docs/cli/cloud.mdx +11 -9
  360. package/docs/cli/doctor.mdx +1 -0
  361. package/docs/cli/init.mdx +1 -0
  362. package/docs/cli/report.mdx +44 -0
  363. package/docs/cli/status.mdx +35 -20
  364. package/docs/cli.mdx +8 -18
  365. package/docs/desktop.mdx +1 -1
  366. package/docs/faq.mdx +1 -1
  367. package/docs/flows.md +11 -0
  368. package/docs/frameworks.mdx +113 -1
  369. package/docs/install-agentic.mdx +36 -0
  370. package/docs/install-manual.mdx +1 -1
  371. package/docs/packages/tauri.mdx +1 -1
  372. package/docs/packages.mdx +1 -1
  373. package/docs/predicates.mdx +144 -4
  374. package/docs/system-map.md +6 -4
  375. package/docs/telemetry.md +2 -1
  376. package/docs/token-efficiency.md +2 -2
  377. package/docs/tools/overview.mdx +1 -1
  378. package/docs/tools/query.mdx +10 -10
  379. package/docs/troubleshooting.mdx +8 -2
  380. package/docs/usage.md +2 -2
  381. package/docs/what-is-recorded.md +62 -0
  382. package/package.json +5 -5
  383. package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
  384. package/dist/judgement/outcome/predicate-to-expect.js +0 -148
@@ -26,20 +26,35 @@ export function routesFromEvents(events) {
26
26
  return route === undefined ? [] : [route];
27
27
  });
28
28
  }
29
- /** Persist initial and subsequent routes for existing and future browser sessions. */
30
- export function attachRouteLearning(bridge, project) {
29
+ /**
30
+ * Persist initial and subsequent routes for existing and future browser sessions.
31
+ *
32
+ * `storeFor` resolves the store for ONE session's artifact root, rather than the daemon being handed
33
+ * a single store bound to its own `process.cwd()`. That binding was right only when the daemon
34
+ * happened to be started inside the app it was driving — and a user-scoped MCP registration, the
35
+ * common case, starts it wherever the editor's cwd is. The routes of every app then landed in that
36
+ * directory, which is how a `.reticle/` kept reappearing in a backend nobody had instrumented,
37
+ * while the driven project's own memory never filled.
38
+ */
39
+ export function attachRouteLearning(bridge, storeFor) {
31
40
  const attached = new WeakSet();
32
- const pending = new Set();
41
+ // Keyed by STORE rather than by root path: `storeFor` already answers one store per root, so this
42
+ // cannot disagree with it about which roots are distinct.
43
+ const pending = new Map();
33
44
  let flushTimer;
34
45
  const flush = () => {
35
46
  flushTimer = undefined;
36
- const routes = [...pending];
47
+ const batches = [...pending];
37
48
  pending.clear();
38
- if (routes.length > 0)
39
- void project.recordRoutes(routes).catch(() => undefined);
49
+ for (const [store, routes] of batches) {
50
+ if (routes.size > 0)
51
+ void store.recordRoutes([...routes]).catch(() => undefined);
52
+ }
40
53
  };
41
- const queue = (route) => {
42
- pending.add(route);
54
+ const queue = (store, route) => {
55
+ const routes = pending.get(store) ?? new Set();
56
+ routes.add(route);
57
+ pending.set(store, routes);
43
58
  if (flushTimer !== undefined)
44
59
  clearTimeout(flushTimer);
45
60
  flushTimer = setTimeout(flush, ROUTE_PERSIST_DEBOUNCE_MS);
@@ -48,13 +63,14 @@ export function attachRouteLearning(bridge, project) {
48
63
  if (attached.has(session))
49
64
  return;
50
65
  attached.add(session);
66
+ const store = storeFor(session.artifactRoot);
51
67
  const initial = routeFromUrl(session.url);
52
68
  if (initial !== undefined)
53
- queue(initial);
69
+ queue(store, initial);
54
70
  session.onEvent((event) => {
55
71
  const route = routeFromEvent(event);
56
72
  if (route !== undefined)
57
- queue(route);
73
+ queue(store, route);
58
74
  });
59
75
  };
60
76
  for (const session of bridge.sessions.all())
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The project memory store for ONE artifact root.
3
+ *
4
+ * `deps.project` is bound to the daemon's own root at construction, which is right only when the
5
+ * daemon happens to have been started inside the app it is driving — and a user-scoped MCP
6
+ * registration, the common case, starts it wherever the editor's cwd is. So every run record, every
7
+ * learned route and every observability high-water mark was written into that directory: a
8
+ * `.reticle/` appearing in a backend nobody instrumented, and a driven project whose own memory
9
+ * stayed empty. The cloud half of `recordReplayRun` had already been moved onto the session's root
10
+ * and its local half left behind, which is how a replay outcome could reach the dashboard and not
11
+ * the project it belonged to.
12
+ *
13
+ * Reads and writes both go through here, deliberately: a write that resolves to the app while a
14
+ * read still answers from the daemon's directory is worse than either alone — the record lands and
15
+ * is then invisible to the tool that exists to report it.
16
+ *
17
+ * Rebinding is cheap (a filesystem port, a root and a clock) and the single-project case allocates
18
+ * nothing: when the resolved root IS the daemon's, `deps.project` comes back unchanged.
19
+ */
20
+ import { ProjectStore } from './project-store.js';
21
+ import type { FileSystemPort } from './fs/fs-port.js';
22
+ import type { ToolDeps } from '../../surface/tools/tool-kit.js';
23
+ export declare function projectForRoot(deps: ToolDeps, root: string): ProjectStore;
24
+ /**
25
+ * The same question asked by the daemon rather than by a tool: one store per artifact root, so a
26
+ * session's learned routes land in the app that was driven and not in whatever directory the daemon
27
+ * was launched from. The daemon's own store is seeded in, so the single-project case allocates
28
+ * nothing and keeps the instance every other caller already holds.
29
+ */
30
+ export declare function projectStoreResolver(fs: FileSystemPort, daemonStore: ProjectStore, daemonRoot: string, now: () => number): (root: string | undefined) => ProjectStore;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The project memory store for ONE artifact root.
3
+ *
4
+ * `deps.project` is bound to the daemon's own root at construction, which is right only when the
5
+ * daemon happens to have been started inside the app it is driving — and a user-scoped MCP
6
+ * registration, the common case, starts it wherever the editor's cwd is. So every run record, every
7
+ * learned route and every observability high-water mark was written into that directory: a
8
+ * `.reticle/` appearing in a backend nobody instrumented, and a driven project whose own memory
9
+ * stayed empty. The cloud half of `recordReplayRun` had already been moved onto the session's root
10
+ * and its local half left behind, which is how a replay outcome could reach the dashboard and not
11
+ * the project it belonged to.
12
+ *
13
+ * Reads and writes both go through here, deliberately: a write that resolves to the app while a
14
+ * read still answers from the daemon's directory is worse than either alone — the record lands and
15
+ * is then invisible to the tool that exists to report it.
16
+ *
17
+ * Rebinding is cheap (a filesystem port, a root and a clock) and the single-project case allocates
18
+ * nothing: when the resolved root IS the daemon's, `deps.project` comes back unchanged.
19
+ */
20
+ import { ProjectStore } from './project-store.js';
21
+ export function projectForRoot(deps, root) {
22
+ if (root === deps.reticleRoot)
23
+ return deps.project;
24
+ return new ProjectStore(deps.fs, root, { now: deps.now });
25
+ }
26
+ /**
27
+ * The same question asked by the daemon rather than by a tool: one store per artifact root, so a
28
+ * session's learned routes land in the app that was driven and not in whatever directory the daemon
29
+ * was launched from. The daemon's own store is seeded in, so the single-project case allocates
30
+ * nothing and keeps the instance every other caller already holds.
31
+ */
32
+ export function projectStoreResolver(fs, daemonStore, daemonRoot, now) {
33
+ const byRoot = new Map([[daemonRoot, daemonStore]]);
34
+ return (root) => {
35
+ const key = root ?? daemonRoot;
36
+ let found = byRoot.get(key);
37
+ if (found === undefined) {
38
+ found = new ProjectStore(fs, key, { now });
39
+ byRoot.set(key, found);
40
+ }
41
+ return found;
42
+ };
43
+ }
@@ -1,4 +1,6 @@
1
1
  import { z } from 'zod';
2
+ import { sessionRoot } from './session-root.js';
3
+ import { projectForRoot } from './project-for-root.js';
2
4
  import { ProjectReadError, RunStatus } from '@reticlehq/core';
3
5
  import { ReticleTool } from '@reticlehq/core';
4
6
  import { countSchema } from '../../surface/tools/args/numeric-bounds.js';
@@ -67,9 +69,11 @@ function lastTwoFor(runs, name) {
67
69
  * The per-flow diff between the two most-recent verification ARTIFACTS (.reticle/runs), or undefined when
68
70
  * fewer than two exist. Never throws — a missing/unreadable artifact must not break reading run history.
69
71
  */
70
- async function lastTwoRunArtifacts(deps) {
72
+ async function lastTwoRunArtifacts(deps,
73
+ /** The session's root — the same address `persistAndSyncVerificationRun` wrote the pair to. */
74
+ root) {
71
75
  try {
72
- const pair = await new RunStore(deps.fs, deps.reticleRoot).latestTwo();
76
+ const pair = await new RunStore(deps.fs, root).latestTwo();
73
77
  return pair === undefined ? undefined : diffVerificationRuns(pair[0], pair[1]);
74
78
  }
75
79
  catch {
@@ -106,7 +110,8 @@ export const PROJECT_TOOLS = [
106
110
  handler: async (deps, args) => {
107
111
  const cloud = await cloudRegression(deps, asString(args['sessionId']));
108
112
  const withCloud = (obj) => cloud === undefined ? obj : { ...obj, cloud };
109
- const read = await deps.project.read();
113
+ const project = projectForRoot(deps, sessionRoot(deps, asString(args['sessionId'])));
114
+ const read = await project.read();
110
115
  if (!read.ok) {
111
116
  return withCloud({
112
117
  error: read.reason === ProjectReadError.MISSING
@@ -133,14 +138,14 @@ export const PROJECT_TOOLS = [
133
138
  learned: read.file.learned,
134
139
  });
135
140
  }
136
- const lastRun = await deps.project.lastRun(name);
141
+ const lastRun = await project.lastRun(name);
137
142
  const pair = lastTwoFor(read.file.runs, name);
138
143
  //: the RICH run diff (per-flow duration deltas past a noise floor, status changes, new/removed
139
144
  // flows, verdict change) over the last two verification ARTIFACTS. That lives alongside — not
140
145
  // instead of — the lightweight RunRecord diff above: `diff` answers "did this named run behave like
141
146
  // last time?" from project.json, while `runDiff` answers "what changed between the last two full
142
147
  // verification runs?" from .reticle/runs. Best-effort: no artifacts simply means no runDiff.
143
- const runDiff = await lastTwoRunArtifacts(deps);
148
+ const runDiff = await lastTwoRunArtifacts(deps, sessionRoot(deps, asString(args['sessionId'])));
144
149
  const forName = read.file.runs.filter((r) => r.name === name);
145
150
  return withCloud({
146
151
  runs: recent(forName),
@@ -1,3 +1,4 @@
1
+ import type { ProjectId } from '@reticlehq/core';
1
2
  import type { ToolDeps } from '../../surface/tools/tool-kit.js';
2
3
  /**
3
4
  * The `.reticle` directory a tool call's artifacts belong in.
@@ -9,15 +10,43 @@ import type { ToolDeps } from '../../surface/tools/tool-kit.js';
9
10
  * fixed — the flow would save successfully and then be invisible to the tool that exists to replay
10
11
  * it, which reads as "no flows covered this change" rather than as an error.
11
12
  *
12
- * Falls back to `deps.reticleRoot` whenever resolution cannot name a project, which is exactly the
13
- * behaviour every call site had before the resolver existed.
13
+ * Falls back to `deps.reticleRoot` whenever resolution cannot name a project — EXCEPT when the
14
+ * caller named a session that resolves to nothing, which is the one case where the fallback is a
15
+ * wrong answer rather than a cautious one. See `sessionProjectId`.
14
16
  */
15
17
  export declare function sessionRoot(deps: ToolDeps, sessionId: string | undefined): string;
18
+ /**
19
+ * The same answer for a caller that already holds the projectId rather than a sessionId.
20
+ *
21
+ * The suite paths resolve the project ONCE and thread it down -- the run artifact, the cloud link,
22
+ * the flake ledger and the flows all belong to that one project -- so asking them to go back to the
23
+ * session manager for an id they already have is how a caller ends up taking the root from one
24
+ * place and the id from another. `sessionRoot` is now this function plus a lookup, so the two
25
+ * cannot disagree about what a resolved root is.
26
+ */
27
+ export declare function rootForProjectId(deps: ToolDeps, projectId: ProjectId | undefined): string;
28
+ /**
29
+ * The PROJECT directory -- one level above `.reticle` -- for a caller that runs a tool in a tree
30
+ * rather than writing a file into one. `git diff` is the case: it was being run in the daemon's
31
+ * `.reticle` subdirectory, which answers about the wrong repository (or, from a globally-registered
32
+ * daemon at `/` or `$HOME`, about none) while reporting a clean "nothing changed".
33
+ */
34
+ export declare function projectDirFor(deps: ToolDeps, sessionId: string | undefined): string;
16
35
  /**
17
36
  * The connected session's projectId, or undefined when there is no session to ask.
18
37
  *
19
- * `sessions.resolve` throws when nothing is connected, when the id names no session, and when
20
- * several are connected and none was named. All three mean the same thing here — we cannot tell
21
- * which project this call is about — and none of them is a reason to fail the caller's tool.
38
+ * `sessions.resolve` throws for three reasons, and they do not all mean the same thing. Nothing
39
+ * connected, and several connected with none named, both mean "we cannot tell which project this
40
+ * call is about" — on a call that named no session that is not a reason to fail the caller's tool,
41
+ * which may not need a session at all, so it degrades to the daemon's own root.
42
+ *
43
+ * A sessionId that names NO session is the third, and it is a different fact. An agent that passes
44
+ * an id has named the project too, so swallowing that throw does not degrade to a cautious answer:
45
+ * it silently retargets the call at whichever checkout the daemon was started in (#994).
46
+ * The same fallback can expose another project's ledger on reads as well as misdirect writes.
47
+ *
48
+ * So the ambiguous case still declines (the resolver has its own `AMBIGUOUS` branch for the same
49
+ * reason) and the mis-addressed case refuses. There is no root that is right for a dead id, so
50
+ * there is no answer either.
22
51
  */
23
- export declare function sessionProjectId(deps: ToolDeps, sessionId: string | undefined): string | undefined;
52
+ export declare function sessionProjectId(deps: ToolDeps, sessionId: string | undefined): ProjectId | undefined;
@@ -1,3 +1,4 @@
1
+ import { dirname } from 'node:path';
1
2
  /**
2
3
  * The `.reticle` directory a tool call's artifacts belong in.
3
4
  *
@@ -8,18 +9,62 @@
8
9
  * fixed — the flow would save successfully and then be invisible to the tool that exists to replay
9
10
  * it, which reads as "no flows covered this change" rather than as an error.
10
11
  *
11
- * Falls back to `deps.reticleRoot` whenever resolution cannot name a project, which is exactly the
12
- * behaviour every call site had before the resolver existed.
12
+ * Falls back to `deps.reticleRoot` whenever resolution cannot name a project — EXCEPT when the
13
+ * caller named a session that resolves to nothing, which is the one case where the fallback is a
14
+ * wrong answer rather than a cautious one. See `sessionProjectId`.
13
15
  */
14
16
  export function sessionRoot(deps, sessionId) {
15
- return deps.artifactRootFor?.(sessionProjectId(deps, sessionId)).root ?? deps.reticleRoot;
17
+ // Skip the LOOKUP, never the refusal.
18
+ //
19
+ // `artifactRootFor?.(…)` short-circuits its own argument, so an unwired caller used to skip
20
+ // resolution entirely and hand a named-but-dead session the daemon's own directory without ever
21
+ // checking the id -- which is how one app's intents, flows and runs land in another app's
22
+ // checkout. Resolving as an ARGUMENT fixes that, because arguments are evaluated first.
23
+ //
24
+ // But `resolve(undefined)` is not free: it builds the ranked no-session diagnosis and records
25
+ // which branch it took. With no resolver wired AND no id named there is nothing to refuse and
26
+ // nothing its answer could change, so that one case skips the lookup and costs what it always
27
+ // did. Every embedder of this engine, and every older construction of ToolDeps, is on it.
28
+ const nothingToRefuse = deps.artifactRootFor === undefined && sessionId === undefined;
29
+ return rootForProjectId(deps, nothingToRefuse ? undefined : sessionProjectId(deps, sessionId));
30
+ }
31
+ /**
32
+ * The same answer for a caller that already holds the projectId rather than a sessionId.
33
+ *
34
+ * The suite paths resolve the project ONCE and thread it down -- the run artifact, the cloud link,
35
+ * the flake ledger and the flows all belong to that one project -- so asking them to go back to the
36
+ * session manager for an id they already have is how a caller ends up taking the root from one
37
+ * place and the id from another. `sessionRoot` is now this function plus a lookup, so the two
38
+ * cannot disagree about what a resolved root is.
39
+ */
40
+ export function rootForProjectId(deps, projectId) {
41
+ return deps.artifactRootFor?.(projectId).root ?? deps.reticleRoot;
42
+ }
43
+ /**
44
+ * The PROJECT directory -- one level above `.reticle` -- for a caller that runs a tool in a tree
45
+ * rather than writing a file into one. `git diff` is the case: it was being run in the daemon's
46
+ * `.reticle` subdirectory, which answers about the wrong repository (or, from a globally-registered
47
+ * daemon at `/` or `$HOME`, about none) while reporting a clean "nothing changed".
48
+ */
49
+ export function projectDirFor(deps, sessionId) {
50
+ return dirname(sessionRoot(deps, sessionId));
16
51
  }
17
52
  /**
18
53
  * The connected session's projectId, or undefined when there is no session to ask.
19
54
  *
20
- * `sessions.resolve` throws when nothing is connected, when the id names no session, and when
21
- * several are connected and none was named. All three mean the same thing here — we cannot tell
22
- * which project this call is about — and none of them is a reason to fail the caller's tool.
55
+ * `sessions.resolve` throws for three reasons, and they do not all mean the same thing. Nothing
56
+ * connected, and several connected with none named, both mean "we cannot tell which project this
57
+ * call is about" — on a call that named no session that is not a reason to fail the caller's tool,
58
+ * which may not need a session at all, so it degrades to the daemon's own root.
59
+ *
60
+ * A sessionId that names NO session is the third, and it is a different fact. An agent that passes
61
+ * an id has named the project too, so swallowing that throw does not degrade to a cautious answer:
62
+ * it silently retargets the call at whichever checkout the daemon was started in (#994).
63
+ * The same fallback can expose another project's ledger on reads as well as misdirect writes.
64
+ *
65
+ * So the ambiguous case still declines (the resolver has its own `AMBIGUOUS` branch for the same
66
+ * reason) and the mis-addressed case refuses. There is no root that is right for a dead id, so
67
+ * there is no answer either.
23
68
  */
24
69
  /*
25
70
  * Exported alongside `sessionRoot`, and that is the point rather than a convenience. A caller that
@@ -32,7 +77,24 @@ export function sessionProjectId(deps, sessionId) {
32
77
  try {
33
78
  return deps.sessions.resolve(sessionId).projectId;
34
79
  }
35
- catch {
36
- return undefined;
80
+ catch (cause) {
81
+ if (sessionId === undefined)
82
+ return undefined;
83
+ throw unresolvedTargetRefusal(sessionId, cause);
37
84
  }
38
85
  }
86
+ /**
87
+ * Why a named-but-dead session gets no directory, and what to do instead.
88
+ *
89
+ * The manager's own message is carried verbatim at the end because it is the actionable half — it
90
+ * names the sessions that ARE connected, with their ids and urls, or says that none are. Rebuilding
91
+ * a worse version of that here is how the two no-session paths drifted apart the last time.
92
+ */
93
+ function unresolvedTargetRefusal(sessionId, cause) {
94
+ const because = cause instanceof Error ? cause.message : String(cause);
95
+ return new Error(`sessionId '${sessionId}' names no session, so there is no project to resolve this call's ` +
96
+ '.reticle directory against, and nothing was read or written. Reticle refuses rather than ' +
97
+ "falling back to the daemon's own directory, which belongs to whatever project the daemon " +
98
+ "was started in — that is how one app's intents, flows and runs land in another app's " +
99
+ `checkout. Omit sessionId to use the connected app, or name one that is live. ${because}`);
100
+ }
@@ -63,3 +63,9 @@ export declare function hasAnyAppConnectedBefore(stateDir: string, port: number,
63
63
  * diagnosis, which is why it now shares this function instead of keeping a second copy of the rule.
64
64
  */
65
65
  export declare function hasProjectConnectedBefore(stateDir: string, port: number, projectId: string | undefined): boolean;
66
+ /**
67
+ * Has any of these projects connected before: the first-move question for a directory that speaks
68
+ * for more than one app (a monorepo root), or for none by name (an app wired by the plugin alone).
69
+ * The ids come from `projectIdsAt`; each is still asked the narrow question.
70
+ */
71
+ export declare function hasAnyProjectConnectedBefore(stateDir: string, port: number, projectIds: readonly string[]): boolean;
@@ -112,3 +112,11 @@ export function hasProjectConnectedBefore(stateDir, port, projectId) {
112
112
  return false;
113
113
  return hasAnyAppConnectedBefore(stateDir, port, projectId);
114
114
  }
115
+ /**
116
+ * Has any of these projects connected before: the first-move question for a directory that speaks
117
+ * for more than one app (a monorepo root), or for none by name (an app wired by the plugin alone).
118
+ * The ids come from `projectIdsAt`; each is still asked the narrow question.
119
+ */
120
+ export function hasAnyProjectConnectedBefore(stateDir, port, projectIds) {
121
+ return projectIds.some((id) => hasProjectConnectedBefore(stateDir, port, id));
122
+ }
@@ -1,5 +1,5 @@
1
1
  /** One project this machine has paired before, in the shape the no-session facts already carry. */
2
- export interface RegisteredProject {
2
+ interface RegisteredProject {
3
3
  directory: string;
4
4
  projectId?: string;
5
5
  }
@@ -52,3 +52,4 @@ export declare function isUnscopedRoot(directory: string, home: string): boolean
52
52
  * reporting it as such would tell a correctly-scoped daemon to go and look somewhere else.
53
53
  */
54
54
  export declare function registeredElsewhere(home: string, directory: string): RegisteredProject[];
55
+ export {};
@@ -20,4 +20,13 @@
20
20
  * With no evidence — a fresh daemon, or a HELLO naming no project — nothing is inferred and the
21
21
  * plain reason stands, unless the page sent no token.
22
22
  */
23
+ /**
24
+ * Whether a recorded close is a pairing-token refusal.
25
+ *
26
+ * The bridge records the specific sentence it sent on the socket, not one fixed string, so a
27
+ * comparison with a single constant misses a wrong token, a missing token, and a daemon that
28
+ * belongs to another project. All three are refusals. A later good session is a separate question
29
+ * the caller answers with `connectedSinceLastClosure`.
30
+ */
31
+ export declare function isAuthRefusalReason(reason: string | undefined): boolean;
23
32
  export declare function authFailureReason(servedProjects: ReadonlySet<string>, helloProject: string | undefined, helloToken?: string): string;
@@ -28,10 +28,27 @@ const PLAIN = 'authentication failed';
28
28
  * before `~/.reticle` was replaced. Both need the same first move: ask `status`, which can say which.
29
29
  */
30
30
  const WRONG_TOKEN = 'authentication failed: wrong pairing token — run `reticle status` for the cause';
31
- /** A paste-in snippet or a Next config that never saw a token. Reload cannot mint one. */
32
- const NO_TOKEN = 'authentication failed: no pairing token on the page';
31
+ /**
32
+ * A paste-in snippet or a Next config that never saw a token. Reload cannot mint one; restarting the
33
+ * dev server re-runs the substitution that should have put it there (#1047).
34
+ */
35
+ const NO_TOKEN = 'authentication failed: no pairing token on the page; the build did not substitute it, so restart the dev server';
33
36
  /** WebSocket close reasons are capped at 123 bytes; a longer one throws and closes with nothing. */
34
37
  const MAX_REASON_BYTES = 123;
38
+ const DIFFERENT_PROJECT_PREFIX = 'this daemon serves a different project';
39
+ /**
40
+ * Whether a recorded close is a pairing-token refusal.
41
+ *
42
+ * The bridge records the specific sentence it sent on the socket, not one fixed string, so a
43
+ * comparison with a single constant misses a wrong token, a missing token, and a daemon that
44
+ * belongs to another project. All three are refusals. A later good session is a separate question
45
+ * the caller answers with `connectedSinceLastClosure`.
46
+ */
47
+ export function isAuthRefusalReason(reason) {
48
+ if (reason === undefined)
49
+ return false;
50
+ return reason.startsWith('authentication failed') || reason.startsWith(DIFFERENT_PROJECT_PREFIX);
51
+ }
35
52
  export function authFailureReason(servedProjects, helloProject, helloToken) {
36
53
  // ONE served project, not merely some. A daemon that has accepted sessions from SEVERAL projects
37
54
  // is demonstrably not owned by any one of them, so "that daemon is not yours, stop it" is the
@@ -45,7 +62,7 @@ export function authFailureReason(servedProjects, helloProject, helloToken) {
45
62
  if (helloProject !== undefined &&
46
63
  1 === servedProjects.size &&
47
64
  !servedProjects.has(helloProject)) {
48
- const reason = `this daemon serves a different project — run \`reticle stop\` and retry`;
65
+ const reason = `${DIFFERENT_PROJECT_PREFIX} — run \`reticle stop\` and retry`;
49
66
  return Buffer.byteLength(reason, 'utf8') <= MAX_REASON_BYTES ? reason : PLAIN;
50
67
  }
51
68
  if (helloToken === undefined || 0 === helloToken.length)
@@ -20,7 +20,7 @@ export declare const WS_CLOSE_REASON: {
20
20
  * this stays as the close-code table's label and as the fallback when the skew cannot be read.
21
21
  */
22
22
  readonly PROTOCOL_MISMATCH: "protocol version mismatch — @reticlehq/browser and server disagree";
23
- readonly AUTH_FAILED: "authentication failed — reload the page to pick up the current pairing token";
23
+ readonly AUTH_FAILED: "authentication failed — restart the dev server; a reload keeps the old pairing token";
24
24
  /**
25
25
  * The daemon threw while handling a message. Closing is deliberate: an exception leaves the
26
26
  * session in a state neither side can describe, and a socket left OPEN and unresponsive is the
@@ -97,6 +97,11 @@ export declare class Bridge {
97
97
  * is to make something happen sooner than it already would.
98
98
  */
99
99
  attachSyncRequest(handler: () => void): void;
100
+ /**
101
+ * Register a handler for the panel's harness switch. Optional for the same reason as the above: a
102
+ * bridge built without one ignores the request rather than refusing it.
103
+ */
104
+ attachHarnessRequest(handler: (enabled: boolean) => void): void;
100
105
  /** Register a handler to run when a session connects. Additive — every handler runs. */
101
106
  attachSessionCreate(handler: (session: Session) => void): void;
102
107
  close(): Promise<void>;
@@ -48,7 +48,7 @@ export const WS_CLOSE_REASON = {
48
48
  * this stays as the close-code table's label and as the fallback when the skew cannot be read.
49
49
  */
50
50
  PROTOCOL_MISMATCH: 'protocol version mismatch — @reticlehq/browser and server disagree',
51
- AUTH_FAILED: 'authentication failed — reload the page to pick up the current pairing token',
51
+ AUTH_FAILED: 'authentication failed — restart the dev server; a reload keeps the old pairing token',
52
52
  /**
53
53
  * The daemon threw while handling a message. Closing is deliberate: an exception leaves the
54
54
  * session in a state neither side can describe, and a socket left OPEN and unresponsive is the
@@ -119,6 +119,24 @@ function replayRequest(event) {
119
119
  const name = event.data['text'];
120
120
  return 'string' === typeof name && name.length > 0 ? name : undefined;
121
121
  }
122
+ /**
123
+ * The desired harness state if this event is the panel's switch, else undefined.
124
+ *
125
+ * Carries `on`/`off` rather than meaning "toggle": a double-click on a flaky connection sends two,
126
+ * and two toggles land back where they started while two `on`s are the same as one.
127
+ */
128
+ function harnessRequest(event) {
129
+ if (event.type !== EventType.HUMAN_CONTROL)
130
+ return undefined;
131
+ if (event.data['kind'] !== HumanControlKind.HARNESS)
132
+ return undefined;
133
+ const want = event.data['text'];
134
+ if ('on' === want)
135
+ return true;
136
+ if ('off' === want)
137
+ return false;
138
+ return undefined;
139
+ }
122
140
  /** True when this event is the panel's "sync now" button. Pure boundary narrowing, like the above. */
123
141
  function isSyncRequest(event) {
124
142
  return event.type === EventType.HUMAN_CONTROL && event.data['kind'] === HumanControlKind.SYNC;
@@ -225,6 +243,7 @@ export class Bridge {
225
243
  #onSessionEnd;
226
244
  /** Wired by the daemon: push to the dashboard now, because somebody asked in the panel. */
227
245
  #onSyncRequest;
246
+ #onHarnessRequest;
228
247
  constructor(options) {
229
248
  const host = options.host ?? LOOPBACK_HOST;
230
249
  if ((options.token?.length ?? 0) > TRANSPORT_LIMITS.MAX_TOKEN_LENGTH) {
@@ -479,7 +498,7 @@ export class Bridge {
479
498
  presented: parsed.token !== undefined && 0 < parsed.token.length,
480
499
  tokenSource: this.#tokenSource,
481
500
  });
482
- this.sessions.noteClosure(WS_CLOSE_REASON.AUTH_FAILED, this.#clock());
501
+ this.sessions.noteClosure(reason, this.#clock());
483
502
  socket.close(WS_CLOSE.AUTH_FAILED[0], reason);
484
503
  return;
485
504
  }
@@ -616,6 +635,10 @@ export class Bridge {
616
635
  // A panel ▶ replay needs the daemon's flow store, which the Session can't reach — route it to
617
636
  // the daemon-wired handler instead of the in-session control path. Everything else is normal.
618
637
  const replay = replayRequest(parsed.event);
638
+ // Narrowed before the chain, not inside it: the result is needed by the branch AND by the
639
+ // test that decides whether this is a panel control at all, and calling it twice would
640
+ // invite the two calls to disagree.
641
+ const harness = harnessRequest(parsed.event);
619
642
  if (replay !== undefined)
620
643
  this.#onReplay?.(session.id, replay);
621
644
  // Same shape as ▶ replay, and routed the same way: what to push lives in the daemon's
@@ -623,6 +646,10 @@ export class Bridge {
623
646
  // us to hurry, not asking for a result.
624
647
  else if (isSyncRequest(parsed.event))
625
648
  this.#onSyncRequest?.();
649
+ // The switch is written through to the platform by the daemon, for the same reason: the
650
+ // Session has no cloud credential and should not grow one.
651
+ else if (harness !== undefined)
652
+ this.#onHarnessRequest?.(harness);
626
653
  // Pass the raw frame's byte length so the buffer doesn't re-serialize every event for accounting.
627
654
  else
628
655
  session.pushEvent(parsed.event, Buffer.byteLength(text, 'utf8'));
@@ -758,6 +785,13 @@ export class Bridge {
758
785
  attachSyncRequest(handler) {
759
786
  this.#onSyncRequest = handler;
760
787
  }
788
+ /**
789
+ * Register a handler for the panel's harness switch. Optional for the same reason as the above: a
790
+ * bridge built without one ignores the request rather than refusing it.
791
+ */
792
+ attachHarnessRequest(handler) {
793
+ this.#onHarnessRequest = handler;
794
+ }
761
795
  /** Register a handler to run when a session connects. Additive — every handler runs. */
762
796
  attachSessionCreate(handler) {
763
797
  this.#onSessionCreate.push(handler);
@@ -7,6 +7,7 @@ import { classifyConnectFailure } from '../../telemetry/connect-failure.js';
7
7
  import { ActionType, DriveErrorCode, DRIVE_PLAYWRIGHT_MISSING_MSG } from '@reticlehq/core';
8
8
  import { installNetworkMocks } from './network-mock.js';
9
9
  import { attachNetworkDetail } from './network-detail.js';
10
+ import { injectedConnectArgs } from '../pool/zero-install.js';
10
11
  /** Structured, code-tagged failure so callers branch on cause, not message text. */
11
12
  export class DriveError extends Error {
12
13
  code;
@@ -338,7 +339,7 @@ export class LaunchedRealInputProvider {
338
339
  await page.waitForFunction('!!globalThis.__reticleInstance', {
339
340
  timeout: INJECT_CONNECT_WAIT_MS,
340
341
  });
341
- const arg = JSON.stringify({ allowNonLocalhost: true, token: opts.token, url: opts.url });
342
+ const arg = JSON.stringify(injectedConnectArgs(opts));
342
343
  await page.evaluate(`globalThis.__reticleInstance.connect(${arg})`);
343
344
  }
344
345
  catch {
@@ -18,6 +18,11 @@ export interface Lease {
18
18
  readonly url: string;
19
19
  readonly navStatus?: number;
20
20
  release(): Promise<void>;
21
+ /**
22
+ * Hand the page the zero-install reader: evaluated in the live document, and added ahead of every
23
+ * document this lease loads next. False when there is no reader, or the page cannot run one.
24
+ */
25
+ injectReader?(): Promise<boolean>;
21
26
  }
22
27
  /** Default lease time-to-live: a lease untouched for this long is presumed orphaned and reclaimed. */
23
28
  export declare const DEFAULT_LEASE_TTL_MS: number;
@@ -32,6 +37,11 @@ interface BrowserPoolOptions {
32
37
  leaseTtlMs?: number;
33
38
  /** Per-lease navigation timeout — a page that won't load fails its own lease, never blocks a slot. */
34
39
  navTimeoutMs?: number;
40
+ /**
41
+ * The zero-install reader, built where the daemon knows its own port and pairing token. A thunk so
42
+ * the ~450KB build is only read if some page ever needs it; undefined when there is none to give.
43
+ */
44
+ zeroInstallScript?: () => string | undefined;
35
45
  }
36
46
  /**
37
47
  * Owns a single browser and leases isolated contexts out of it, capped at `maxContexts`.
@@ -30,6 +30,7 @@ export class BrowserPool {
30
30
  #genId;
31
31
  #now;
32
32
  #ttl;
33
+ #zeroInstall;
33
34
  #navTimeout;
34
35
  #browser;
35
36
  #launching;
@@ -70,6 +71,7 @@ export class BrowserPool {
70
71
  this.#now = opts.now ?? (() => Date.now());
71
72
  this.#ttl = opts.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
72
73
  this.#navTimeout = opts.navTimeoutMs ?? DEFAULT_NAV_TIMEOUT_MS;
74
+ this.#zeroInstall = opts.zeroInstallScript;
73
75
  }
74
76
  /** The TTL configured for leases — how long an untouched lease lives before the reaper reclaims it. */
75
77
  leaseTtlMs() {
@@ -405,6 +407,14 @@ export class BrowserPool {
405
407
  url,
406
408
  ...(navStatus !== undefined ? { navStatus } : {}),
407
409
  release: () => this.#release(sessionId),
410
+ injectReader: async () => {
411
+ const script = this.#zeroInstall?.();
412
+ if (script === undefined || page.evaluate === undefined)
413
+ return false;
414
+ await page.addInitScript?.(script);
415
+ await page.evaluate(script);
416
+ return true;
417
+ },
408
418
  };
409
419
  }
410
420
  catch (err) {
@@ -41,6 +41,7 @@ function wrapBrowser(browser) {
41
41
  return {
42
42
  goto: (url, opts) => page.goto(url, gotoOptions(opts?.timeoutMs)),
43
43
  close: () => page.close(),
44
+ evaluate: (script) => page.evaluate(script),
44
45
  // Playwright returns a Buffer; Uint8Array is what the visual store and differ take.
45
46
  screenshot: async (opts) => new Uint8Array(await page.screenshot({ fullPage: true === opts?.fullPage })),
46
47
  // Three moves, same as performGesture: a single move to the center can be a no-op if
@@ -34,6 +34,8 @@ export interface PooledPage {
34
34
  * reports dispatched/settled while the styles never ran.
35
35
  */
36
36
  hover?(x: number, y: number): Promise<void>;
37
+ /** Evaluate a script in the page's current document. OPTIONAL: without it no reader is injected. */
38
+ evaluate?(script: string): Promise<unknown>;
37
39
  /** Add an init script to run after document creation but before any page scripts run. */
38
40
  addInitScript?<Arg>(script: ((arg: Arg) => void) | string, arg?: Arg): Promise<InitScriptHandle | void>;
39
41
  /**