@reticlehq/server 2.7.0 → 2.9.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 (357) hide show
  1. package/README.md +14 -5
  2. package/SKILL.md +130 -778
  3. package/dist/bridge/bridge.d.ts +23 -0
  4. package/dist/bridge/bridge.js +80 -2
  5. package/dist/bridge/bridge.test-harness.d.ts +5 -0
  6. package/dist/bridge/bridge.test-harness.js +6 -4
  7. package/dist/capsule/causal-summary.d.ts +36 -1
  8. package/dist/capsule/causal-summary.js +33 -7
  9. package/dist/capsule/divergence.js +7 -1
  10. package/dist/cli/chromium-hint.d.ts +33 -5
  11. package/dist/cli/chromium-hint.js +82 -12
  12. package/dist/cli/cli-doctor.js +65 -34
  13. package/dist/cli/cli-flow-commands.js +8 -8
  14. package/dist/cli/cli-kill.d.ts +58 -0
  15. package/dist/cli/cli-kill.js +162 -0
  16. package/dist/cli/cli-parse.d.ts +16 -1
  17. package/dist/cli/cli-parse.js +32 -9
  18. package/dist/cli/cli-port.d.ts +32 -0
  19. package/dist/cli/cli-port.js +82 -35
  20. package/dist/cli/cli-verify.d.ts +21 -0
  21. package/dist/cli/cli-verify.js +47 -3
  22. package/dist/cli/config-discovery.d.ts +35 -0
  23. package/dist/cli/config-discovery.js +163 -0
  24. package/dist/cli/doctor-daemon-line.js +4 -1
  25. package/dist/cli/doctor-project-line.d.ts +37 -0
  26. package/dist/cli/doctor-project-line.js +35 -0
  27. package/dist/cli/doctor-rows.d.ts +51 -0
  28. package/dist/cli/doctor-rows.js +54 -0
  29. package/dist/cli/doctor-sessions-line.d.ts +12 -0
  30. package/dist/cli/doctor-sessions-line.js +69 -0
  31. package/dist/cli/drive-attach.d.ts +99 -0
  32. package/dist/cli/drive-attach.js +203 -0
  33. package/dist/cli/drive-command.d.ts +13 -0
  34. package/dist/cli/drive-command.js +103 -0
  35. package/dist/cli/open-note.d.ts +20 -0
  36. package/dist/cli/open-note.js +37 -0
  37. package/dist/cli/port-holder.d.ts +10 -0
  38. package/dist/cli/port-holder.js +26 -2
  39. package/dist/cli/status-next-action.d.ts +37 -0
  40. package/dist/cli/status-next-action.js +61 -0
  41. package/dist/cli.js +119 -39
  42. package/dist/crawl/crawl-tools.js +12 -2
  43. package/dist/crawl/crawl.d.ts +13 -0
  44. package/dist/crawl/crawl.js +50 -2
  45. package/dist/events/contradictions.d.ts +46 -0
  46. package/dist/events/contradictions.js +35 -22
  47. package/dist/events/declared.d.ts +45 -0
  48. package/dist/events/declared.js +73 -0
  49. package/dist/events/echo-mismatch.d.ts +8 -2
  50. package/dist/events/echo-mismatch.js +33 -5
  51. package/dist/events/event-filters.d.ts +1 -1
  52. package/dist/events/event-filters.js +7 -4
  53. package/dist/events/predicate-eval.d.ts +17 -72
  54. package/dist/events/predicate-eval.js +117 -236
  55. package/dist/events/predicate-parse.js +31 -3
  56. package/dist/events/predicate-route.d.ts +32 -0
  57. package/dist/events/predicate-route.js +102 -0
  58. package/dist/events/predicate-schema.d.ts +99 -0
  59. package/dist/events/predicate-schema.js +309 -0
  60. package/dist/events/predicate.d.ts +8 -0
  61. package/dist/events/predicate.js +296 -22
  62. package/dist/flows/annotate-tools.js +3 -0
  63. package/dist/flows/annotate.js +18 -4
  64. package/dist/flows/coverage.d.ts +38 -5
  65. package/dist/flows/coverage.js +30 -4
  66. package/dist/flows/decision.d.ts +15 -0
  67. package/dist/flows/decision.js +1 -1
  68. package/dist/flows/flow-classify.js +1 -1
  69. package/dist/flows/flow-replay-run.js +8 -1
  70. package/dist/flows/flow-tools.js +21 -5
  71. package/dist/flows/heal-run.js +1 -1
  72. package/dist/flows/replay.js +3 -0
  73. package/dist/honesty/accepted-write.d.ts +3 -0
  74. package/dist/honesty/accepted-write.js +8 -3
  75. package/dist/honesty/already-true.js +15 -0
  76. package/dist/honesty/blind-spots.js +10 -2
  77. package/dist/honesty/honesty.d.ts +11 -0
  78. package/dist/honesty/honesty.js +1 -0
  79. package/dist/honesty/uncaptured-bodies.d.ts +4 -2
  80. package/dist/honesty/uncaptured-bodies.js +9 -2
  81. package/dist/honesty/unread-outcome.d.ts +8 -0
  82. package/dist/honesty/unread-outcome.js +36 -9
  83. package/dist/honesty/unsettled.d.ts +49 -0
  84. package/dist/honesty/unsettled.js +82 -0
  85. package/dist/honesty/verified.d.ts +36 -3
  86. package/dist/honesty/verified.js +54 -11
  87. package/dist/http-server.d.ts +19 -0
  88. package/dist/http-server.js +202 -4
  89. package/dist/impact/impact-recorder.d.ts +23 -0
  90. package/dist/impact/impact-recorder.js +76 -0
  91. package/dist/impact/impact-store.d.ts +47 -0
  92. package/dist/impact/impact-store.js +171 -0
  93. package/dist/index.d.ts +7 -0
  94. package/dist/index.js +47 -10
  95. package/dist/init/agent-rules.d.ts +36 -0
  96. package/dist/init/agent-rules.js +132 -22
  97. package/dist/init/astro-patch.d.ts +2 -1
  98. package/dist/init/astro-patch.js +38 -6
  99. package/dist/init/capabilities.d.ts +22 -0
  100. package/dist/init/capabilities.js +76 -0
  101. package/dist/init/confirm.d.ts +114 -0
  102. package/dist/init/confirm.js +165 -0
  103. package/dist/init/connect-steps.d.ts +2 -0
  104. package/dist/init/connect-steps.js +50 -0
  105. package/dist/init/cra.js +3 -2
  106. package/dist/init/csp-check.d.ts +38 -0
  107. package/dist/init/csp-check.js +92 -0
  108. package/dist/init/csp-doctor.d.ts +25 -0
  109. package/dist/init/csp-doctor.js +49 -0
  110. package/dist/init/cursor.js +6 -9
  111. package/dist/init/detect.d.ts +8 -0
  112. package/dist/init/detect.js +33 -3
  113. package/dist/init/mcp-clients.d.ts +1 -0
  114. package/dist/init/mcp-clients.js +51 -7
  115. package/dist/init/mcp.d.ts +15 -0
  116. package/dist/init/mcp.js +19 -0
  117. package/dist/init/non-js-project.js +29 -5
  118. package/dist/init/plan-framework.d.ts +10 -0
  119. package/dist/init/plan-framework.js +78 -16
  120. package/dist/init/plan.d.ts +47 -6
  121. package/dist/init/plan.js +228 -69
  122. package/dist/init/run.d.ts +24 -2
  123. package/dist/init/run.js +157 -45
  124. package/dist/init/slash-command.d.ts +1 -1
  125. package/dist/init/slash-command.js +54 -6
  126. package/dist/init/snippets.d.ts +45 -5
  127. package/dist/init/snippets.js +194 -42
  128. package/dist/init/vite-config.js +53 -5
  129. package/dist/input/network-mock-tools.d.ts +19 -0
  130. package/dist/input/network-mock-tools.js +37 -5
  131. package/dist/input/scroll-find.d.ts +6 -0
  132. package/dist/input/scroll-find.js +17 -2
  133. package/dist/input/scroll-tools.js +4 -0
  134. package/dist/journal/attach-journal.d.ts +9 -1
  135. package/dist/journal/attach-journal.js +13 -2
  136. package/dist/journal/journal-query.d.ts +13 -0
  137. package/dist/journal/journal-query.js +28 -3
  138. package/dist/journal/workspace-gitignore.d.ts +10 -0
  139. package/dist/journal/workspace-gitignore.js +59 -0
  140. package/dist/license/license.js +1 -1
  141. package/dist/mcp/mcp-proxy.d.ts +35 -30
  142. package/dist/mcp/mcp-proxy.js +306 -104
  143. package/dist/mcp/mcp.d.ts +10 -1
  144. package/dist/mcp/mcp.js +70 -34
  145. package/dist/mcp/proxy-handshake.d.ts +82 -1
  146. package/dist/mcp/proxy-handshake.js +109 -2
  147. package/dist/mcp/proxy-lifecycle.d.ts +11 -2
  148. package/dist/mcp/proxy-lifecycle.js +12 -3
  149. package/dist/mcp/proxy-log.d.ts +63 -0
  150. package/dist/mcp/proxy-log.js +132 -0
  151. package/dist/mcp/server-instructions.d.ts +17 -0
  152. package/dist/mcp/server-instructions.js +56 -0
  153. package/dist/mcp/shared-params.d.ts +27 -0
  154. package/dist/mcp/shared-params.js +36 -0
  155. package/dist/pool/browser-pool.d.ts +22 -2
  156. package/dist/pool/browser-pool.js +44 -4
  157. package/dist/project/learned-routes.d.ts +24 -0
  158. package/dist/project/learned-routes.js +63 -0
  159. package/dist/project/project-store.d.ts +6 -0
  160. package/dist/project/project-store.js +25 -1
  161. package/dist/project/reticle-dir.d.ts +2 -0
  162. package/dist/project/reticle-dir.js +1 -0
  163. package/dist/session/connection-memory.d.ts +59 -0
  164. package/dist/session/connection-memory.js +108 -0
  165. package/dist/session/dev-command.d.ts +26 -0
  166. package/dist/session/dev-command.js +89 -0
  167. package/dist/session/dev-server-probe.d.ts +68 -0
  168. package/dist/session/dev-server-probe.js +102 -7
  169. package/dist/session/last-act.d.ts +26 -0
  170. package/dist/session/last-act.js +35 -1
  171. package/dist/session/live-control-tools.js +42 -5
  172. package/dist/session/no-session-diagnosis.d.ts +67 -2
  173. package/dist/session/no-session-diagnosis.js +236 -27
  174. package/dist/session/no-session-next-action.d.ts +46 -0
  175. package/dist/session/no-session-next-action.js +85 -0
  176. package/dist/session/no-session-watch.d.ts +40 -1
  177. package/dist/session/no-session-watch.js +174 -8
  178. package/dist/session/pending-commands.d.ts +12 -0
  179. package/dist/session/pending-commands.js +13 -1
  180. package/dist/session/ref-provenance.d.ts +54 -0
  181. package/dist/session/ref-provenance.js +61 -0
  182. package/dist/session/session-lease.js +1 -1
  183. package/dist/session/session-manager.d.ts +4 -0
  184. package/dist/session/session-manager.js +35 -5
  185. package/dist/session/session.d.ts +10 -1
  186. package/dist/session/session.js +146 -5
  187. package/dist/telemetry/app-instrumented.d.ts +8 -0
  188. package/dist/telemetry/app-instrumented.js +10 -0
  189. package/dist/telemetry/automation-hint.d.ts +17 -0
  190. package/dist/telemetry/automation-hint.js +71 -0
  191. package/dist/telemetry/bug-found.d.ts +15 -17
  192. package/dist/telemetry/bug-found.js +52 -21
  193. package/dist/telemetry/cli-telemetry.js +7 -1
  194. package/dist/telemetry/daemon-telemetry.js +24 -0
  195. package/dist/telemetry/feedback.js +1 -1
  196. package/dist/telemetry/init-telemetry.js +9 -1
  197. package/dist/telemetry/install-source.d.ts +56 -0
  198. package/dist/telemetry/install-source.js +74 -0
  199. package/dist/telemetry/instrumentation-stall.d.ts +50 -0
  200. package/dist/telemetry/instrumentation-stall.js +90 -0
  201. package/dist/telemetry/run-telemetry.js +4 -1
  202. package/dist/telemetry/session-metrics.d.ts +8 -0
  203. package/dist/telemetry/session-metrics.js +15 -0
  204. package/dist/telemetry/telemetry.d.ts +7 -1
  205. package/dist/telemetry/telemetry.js +26 -2
  206. package/dist/telemetry/tool-refused.d.ts +19 -0
  207. package/dist/telemetry/tool-refused.js +55 -0
  208. package/dist/telemetry/verification-of.js +46 -7
  209. package/dist/temp-dir.d.ts +27 -0
  210. package/dist/temp-dir.js +46 -0
  211. package/dist/tools/act-tools.js +209 -28
  212. package/dist/tools/assert-grade.d.ts +1 -1
  213. package/dist/tools/assert-grade.js +27 -5
  214. package/dist/tools/assert-verdict.js +49 -7
  215. package/dist/tools/browser-tools.js +10 -0
  216. package/dist/tools/contract-tools.js +4 -0
  217. package/dist/tools/dynamic-tools.js +16 -7
  218. package/dist/tools/error-recovery.d.ts +10 -0
  219. package/dist/tools/error-recovery.js +73 -9
  220. package/dist/tools/feedback-invite.js +1 -1
  221. package/dist/tools/invoke-tool.js +159 -26
  222. package/dist/tools/lease-hint.d.ts +30 -8
  223. package/dist/tools/lease-hint.js +65 -14
  224. package/dist/tools/lease-tools.d.ts +40 -1
  225. package/dist/tools/lease-tools.js +181 -83
  226. package/dist/tools/navigate-result.js +1 -1
  227. package/dist/tools/observe-tools.js +10 -4
  228. package/dist/tools/observed-nothing.js +1 -1
  229. package/dist/tools/react-grace.d.ts +52 -0
  230. package/dist/tools/react-grace.js +90 -0
  231. package/dist/tools/read-tools.js +67 -5
  232. package/dist/tools/resolve-target.d.ts +33 -0
  233. package/dist/tools/resolve-target.js +53 -0
  234. package/dist/tools/settle-in-flight.d.ts +94 -0
  235. package/dist/tools/settle-in-flight.js +146 -0
  236. package/dist/tools/snapshot-delta.d.ts +10 -0
  237. package/dist/tools/snapshot-delta.js +194 -9
  238. package/dist/tools/tool-kit.d.ts +8 -0
  239. package/dist/tools/tool-surface.d.ts +60 -0
  240. package/dist/tools/tool-surface.js +86 -1
  241. package/dist/tools/tools.js +61 -8
  242. package/dist/tools/unadvertised-help.js +9 -6
  243. package/docs/README.md +55 -11
  244. package/docs/actions.mdx +120 -0
  245. package/docs/agent-cheatsheet.md +95 -29
  246. package/docs/architecture.md +52 -35
  247. package/docs/benchmarks.md +127 -75
  248. package/docs/best-practices.mdx +216 -0
  249. package/docs/capabilities.mdx +534 -0
  250. package/docs/cli/affected.mdx +53 -0
  251. package/docs/cli/capsules.mdx +70 -0
  252. package/docs/cli/cloud.mdx +153 -0
  253. package/docs/cli/doctor.mdx +109 -0
  254. package/docs/cli/drive.mdx +74 -0
  255. package/docs/cli/feedback.mdx +67 -0
  256. package/docs/cli/gate.mdx +64 -0
  257. package/docs/cli/help.mdx +94 -0
  258. package/docs/cli/hunt.mdx +73 -0
  259. package/docs/cli/identify.mdx +76 -0
  260. package/docs/cli/init.mdx +121 -0
  261. package/docs/cli/kill.mdx +91 -0
  262. package/docs/cli/license.mdx +48 -0
  263. package/docs/cli/mcp.mdx +61 -0
  264. package/docs/cli/open.mdx +74 -0
  265. package/docs/cli/restart.mdx +60 -0
  266. package/docs/cli/rollback.mdx +35 -0
  267. package/docs/cli/serve.mdx +69 -0
  268. package/docs/cli/status.mdx +88 -0
  269. package/docs/cli/stop.mdx +64 -0
  270. package/docs/cli/telemetry.mdx +57 -0
  271. package/docs/cli/update.mdx +52 -0
  272. package/docs/cli/verify.mdx +76 -0
  273. package/docs/cli/version.mdx +39 -0
  274. package/docs/cli/watch.mdx +62 -0
  275. package/docs/cli.mdx +241 -0
  276. package/docs/debugging.md +16 -10
  277. package/docs/deploy-checks.md +45 -12
  278. package/docs/desktop.mdx +384 -0
  279. package/docs/docs.json +364 -0
  280. package/docs/enterprise.md +22 -16
  281. package/docs/faq.mdx +102 -0
  282. package/docs/fixtures.md +13 -7
  283. package/docs/flows.md +29 -23
  284. package/docs/for-agents.md +71 -0
  285. package/docs/frameworks.mdx +238 -0
  286. package/docs/gate-plan.md +48 -42
  287. package/docs/gates.md +28 -22
  288. package/docs/getting-started.md +129 -77
  289. package/docs/human-control.md +24 -18
  290. package/docs/index.mdx +143 -0
  291. package/docs/install-agentic.mdx +224 -0
  292. package/docs/install-manual.mdx +196 -0
  293. package/docs/instrumentation.mdx +238 -0
  294. package/docs/integration-patterns.md +41 -35
  295. package/docs/local-registry.md +19 -13
  296. package/docs/multi-agent-testing.md +19 -13
  297. package/docs/packages/babel-plugin.mdx +79 -0
  298. package/docs/packages/browser.mdx +111 -0
  299. package/docs/packages/core.mdx +116 -0
  300. package/docs/packages/electron.mdx +85 -0
  301. package/docs/packages/eslint-plugin.mdx +86 -0
  302. package/docs/packages/next.mdx +89 -0
  303. package/docs/packages/react.mdx +91 -0
  304. package/docs/packages/server.mdx +98 -0
  305. package/docs/packages/tauri.mdx +63 -0
  306. package/docs/packages/test.mdx +119 -0
  307. package/docs/packages/vite-plugin.mdx +94 -0
  308. package/docs/packages.mdx +218 -0
  309. package/docs/platform-integration.md +35 -26
  310. package/docs/predicates.mdx +248 -0
  311. package/docs/quickstart.mdx +257 -0
  312. package/docs/recipes.mdx +282 -0
  313. package/docs/skill-file.mdx +107 -0
  314. package/docs/state-management.mdx +199 -0
  315. package/docs/style.css +14 -0
  316. package/docs/system-map.md +50 -33
  317. package/docs/telemetry-contract.md +106 -52
  318. package/docs/telemetry-events.mdx +475 -0
  319. package/docs/telemetry.md +63 -55
  320. package/docs/testing.md +44 -6
  321. package/docs/token-efficiency.md +37 -27
  322. package/docs/tools-act-and-wait.mdx +173 -0
  323. package/docs/tools-act-sequence.mdx +137 -0
  324. package/docs/tools-act.mdx +95 -0
  325. package/docs/tools-assert.mdx +129 -0
  326. package/docs/tools-console.mdx +63 -0
  327. package/docs/tools-inspect.mdx +88 -0
  328. package/docs/tools-navigate.mdx +99 -0
  329. package/docs/tools-network.mdx +119 -0
  330. package/docs/tools-observe.mdx +145 -0
  331. package/docs/tools-overview.mdx +160 -0
  332. package/docs/tools-query.mdx +126 -0
  333. package/docs/tools-session-and-feedback.mdx +112 -0
  334. package/docs/tools-sessions.mdx +94 -0
  335. package/docs/tools-snapshot.mdx +129 -0
  336. package/docs/tools-state.mdx +104 -0
  337. package/docs/tools-tools-and-run.mdx +156 -0
  338. package/docs/tools-wait-for.mdx +85 -0
  339. package/docs/troubleshooting.mdx +263 -0
  340. package/docs/usage.md +209 -165
  341. package/docs/vs-chrome-devtools-mcp.mdx +123 -0
  342. package/docs/vs-playwright-mcp.mdx +125 -0
  343. package/docs/vs-screenshots.mdx +137 -0
  344. package/docs/what-is-reticle.mdx +63 -0
  345. package/docs/why-reticle.mdx +109 -0
  346. package/package.json +5 -5
  347. package/docs/desktop-apps.md +0 -256
  348. package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
  349. package/docs/matrix/2.5.0/codex-machine.json +0 -20
  350. package/docs/matrix/2.5.0/cursor-machine.json +0 -22
  351. package/docs/matrix/2.5.0/gemini-machine.json +0 -22
  352. package/docs/matrix/2.5.0/opencode-machine.json +0 -22
  353. package/docs/matrix/2.5.0/vscode-machine.json +0 -22
  354. package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
  355. package/docs/matrix/MATRIX.md +0 -26
  356. package/docs/matrix/README.md +0 -59
  357. package/docs/mint.json +0 -61
package/docs/usage.md CHANGED
@@ -1,14 +1,38 @@
1
- # Reticle — Complete Usage Guide
1
+ ---
2
+ title: Complete usage guide
3
+ description: 'The full reference and cookbook: every tool, flag, and workflow, with worked examples.'
4
+ icon: book
5
+ ---
6
+
7
+ Every Reticle drive is the same four steps: **look** (`reticle_snapshot` / `reticle_query`), **act** (`reticle_act` / `reticle_act_sequence`), **observe** (`reticle_observe` / `reticle_network` / `reticle_state`), **assert** (`reticle_assert`). Only `reticle_act_and_wait` and `reticle_assert` produce a verdict, so a run that ends anywhere else proved nothing. This page is the full reference for every tool, predicate, action and flag in that loop.
2
8
 
3
- The full reference and cookbook. If you haven't set up Reticle yet, start with [Getting Started](getting-started.md).
9
+ If you haven't set up Reticle yet, start with [Getting Started](getting-started.md).
10
+
11
+ > **Start here instead, unless you want the long form.** This page predates the focused reference and still holds the fullest single narrative, but almost everything in it now has a page of its own that is shorter, searchable and verified against a running app:
12
+ >
13
+ > | Looking for | Go to |
14
+ > | ------------------------------------------ | --------------------------------------- |
15
+ > | One tool, with a real request and response | [Tools reference](/tools-overview) |
16
+ > | The predicate grammar | [Predicates](/predicates) |
17
+ > | Every action and its arguments | [Actions](/actions) |
18
+ > | Worked examples for real situations | [Recipes](/recipes) |
19
+ > | Baselines, clock, crawl, mocking, flows | [Beyond the verify loop](/capabilities) |
20
+ > | Habits that make a verdict trustworthy | [Best practices](/best-practices) |
21
+ > | Common questions | [FAQ](/faq) |
22
+ >
23
+ > What is still only here: turning existing test cases into agent checks (§11), the security and privacy detail (§15), and real input mode (§18).
24
+
25
+ > **On the notation.** Examples here are written as `reticle_act({ ref, action })`, which is shorthand for "call that tool with these arguments". Over MCP you do not write JavaScript; your client sends the tool name and an arguments object. The shorthand is shorter to read, and the argument shapes are identical either way.
26
+ >
27
+ > If you want examples in the exact JSON your client sends, [Recipes](/recipes) has them, and every response on that page was captured from a running app.
4
28
 
5
29
  **Contents**
6
30
 
7
31
  1. [How Reticle helps you](#1-how-reticle-helps-you)
8
32
  2. [Core concepts](#2-core-concepts)
9
- 3. [The tools — full reference](#3-the-tools--full-reference)
10
- 4. [The predicate DSL — full reference](#4-the-predicate-dsl--full-reference)
11
- 5. [Actions — full list](#5-actions--full-list)
33
+ 3. [The tools: full reference](#3-the-tools-full-reference)
34
+ 4. [The predicate DSL: full reference](#4-the-predicate-dsl-full-reference)
35
+ 5. [Actions: full list](#5-actions-full-list)
12
36
  6. [Snapshot modes & scoping](#6-snapshot-modes--scoping)
13
37
  7. [Cookbook: real situations](#7-cookbook-real-situations)
14
38
  8. [Regression: baselines & diff](#8-regression-baselines--diff)
@@ -19,20 +43,23 @@ The full reference and cookbook. If you haven't set up Reticle yet, start with [
19
43
  13. [Best practices & gotchas](#13-best-practices--gotchas)
20
44
  14. [FAQ](#14-faq)
21
45
  15. [Security & privacy](#15-security--privacy)
46
+ 16. [Presenter mode, narration & fake clock](#16-presenter-mode-narration--fake-clock-watch--control)
47
+ 17. [Evidence-of-effect, act+await, state, capabilities, replay](#17-evidence-of-effect-actawait-state-capabilities-replay)
48
+ 18. [Real input mode: native hover & drag](#18-real-input-mode-native-hover--drag)
22
49
 
23
50
  ---
24
51
 
25
52
  ## 1. How Reticle helps you
26
53
 
27
- You mostly **talk to your agent in plain English** — "add X and verify it works." The agent uses Reticle under the hood. Here's the value, by situation:
54
+ You mostly **talk to your agent in plain English**: "add X and verify it works." The agent uses Reticle under the hood. Here's the value, by situation:
28
55
 
29
- - **You stop being the agent's eyes.** Today you build a feature, then _you_ click through the browser to check it. With Reticle the agent checks its own work and only comes back when it's actually verified — or with a precise reason it failed.
30
- - **Silent breakage gets caught.** A console error, a 500 on one locale, a button that quietly disappeared after a refactor — humans skim past these; Reticle asserts on them.
31
- - **The fix loop closes.** When something's wrong, Reticle reports the _evidence_ — the failing network call, the console stack, and (on React) the **source file:line** to edit.
56
+ - **You stop being the agent's eyes.** Today you build a feature, then _you_ click through the browser to check it. With Reticle the agent checks its own work and only comes back when it's actually verified, or with a precise reason it failed.
57
+ - **Silent breakage gets caught.** A console error, a 500 on one locale, a button that quietly disappeared after a refactor: humans skim past these; Reticle asserts on them.
58
+ - **The fix loop closes.** When something's wrong, Reticle reports the _evidence_: the failing network call, the console stack, and (on React) the **source file:line** to edit.
32
59
  - **It's cheap enough to run constantly.** ~100 tokens per verified interaction means the agent can verify on _every_ edit, not just at the end (see [token-efficiency](token-efficiency.md)).
33
60
  - **Your manual QA becomes automated.** The checklist you never turned into Playwright tests? Your agent runs it now (see [§11](#11-turning-your-test-cases-into-agent-checks)).
34
61
 
35
- Who benefits most: anyone shipping **dashboards, internal tools, SaaS apps** — behavior-heavy UIs with lots of forms, lists, modals, and API calls that change often.
62
+ Who benefits most: anyone shipping **dashboards, internal tools, SaaS apps**. Behavior-heavy UIs with lots of forms, lists, modals, and API calls that change often.
36
63
 
37
64
  ---
38
65
 
@@ -41,32 +68,32 @@ Who benefits most: anyone shipping **dashboards, internal tools, SaaS apps** —
41
68
  **The loop: look → act → observe → assert.**
42
69
 
43
70
  1. **Look** with `reticle_snapshot` (what's on screen) or `reticle_query` (find a specific thing).
44
- 2. **Act** with `reticle_act` (click/fill/…). It returns a `since` cursor — a timestamp marker.
45
- 3. **Observe** with `reticle_observe({ since })` — everything the app did _after_ that action.
46
- 4. **Assert** with `reticle_assert({ predicate })` — verify it, get evidence.
71
+ 2. **Act** with `reticle_act` (click/fill/…). It returns a `since` cursor, a timestamp marker.
72
+ 3. **Observe** with `reticle_observe({ since })`: everything the app did _after_ that action.
73
+ 4. **Assert** with `reticle_assert({ predicate })`. Verify it, get evidence.
47
74
 
48
75
  **Refs.** Elements are addressed by stable handles like `e7`. You get them from `snapshot` or `query`, then pass them to `act`/`inspect`. A ref re-resolves to its element across re-renders; if the element is gone, you get a clear error.
49
76
 
50
- **Evidence, not prose.** Every tool returns structured data — counts, the matching network call, the snapshot delta — so the agent reasons over facts, not a vibe.
77
+ **Evidence, not prose.** Every tool returns structured data (counts, the matching network call, the snapshot delta) so the agent reasons over facts, not a vibe.
51
78
 
52
79
  **Sessions.** Each connected browser tab is a session (named via `reticle.connect({ session })`). With one tab open you never specify it; with several, pass `sessionId`.
53
80
 
54
81
  ---
55
82
 
56
- ## 3. The tools — full reference
83
+ ## 3. The tools: full reference
57
84
 
58
85
  ### `reticle_sessions`
59
86
 
60
- List connected tabs. → `{ sessions: [{ sessionId, url, title, lastSeenMs, hidden, focused, throttled }] }`. `lastSeenMs` is the silence since the tab last reported (not time-since-connect); `throttled` is `true` when the tab is hidden or has gone quiet — a throttled tab silently no-ops timers/rAF/pointer.
87
+ List connected tabs. → `{ sessions: [{ sessionId, url, title, lastSeenMs, hidden, focused, throttled }] }`. `lastSeenMs` is the silence since the tab last reported (not time-since-connect); `throttled` is `true` when the tab is hidden or has gone quiet, and a throttled tab silently no-ops timers/rAF/pointer.
61
88
 
62
89
  ### `reticle_snapshot`
63
90
 
64
91
  A semantic, accessibility-tree view of the page.
65
92
 
66
93
  - **args:** `mode?: 'full' | 'interactive' | 'status'` (default `full`), `scope?` (CSS selector or ref), `diff?: boolean`, `sessionId?`.
67
- - **returns:** `{ tree, status: { route, title, visibleDialogs }, nodes, truncated, cost: { bytes, tokens } }`.
68
- - **`diff: true`** returns only what changed since your last snapshot of the same scope/mode — `{ mode: 'delta', delta: { added, removed, addedCount, removedCount } }` or `{ mode: 'unchanged' }` (no full tree). The first call (and any call after a route change) still returns the full tree. ~99% fewer tokens to re-look after an action; see [token-efficiency.md](token-efficiency.md).
69
- - **`cost`** is an estimated size of the result — re-scope (`mode`/`scope`) before reading if large.
94
+ - **returns:** `{ tree, status: { route, title, visibleDialogs, overlayHidingPage }, nodes, truncated, cost: { bytes, tokens } }`.
95
+ - **`diff: true`** returns only what changed since your last snapshot of the same scope/mode: `{ mode: 'delta', delta: { added, removed, addedCount, removedCount } }` or `{ mode: 'unchanged' }` (no full tree). The first call (and any call after a route change) still returns the full tree. ~99% fewer tokens to re-look after an action; see [token-efficiency.md](token-efficiency.md).
96
+ - **`cost`** is an estimated size of the result. Re-scope (`mode`/`scope`) before reading if large.
70
97
 
71
98
  ```jsonc
72
99
  reticle_snapshot({ mode: "interactive" })
@@ -74,7 +101,7 @@ reticle_snapshot({ mode: "interactive" })
74
101
  // - button "Add item" (ref=e5)
75
102
  // status: { route: "/dashboard", visibleDialogs: [] }
76
103
 
77
- reticle_snapshot({ diff: true }) // after an action — only the change set
104
+ reticle_snapshot({ diff: true }) // after an action: only the change set
78
105
  // { mode: "delta", delta: { added: ['- alert "Saved!"'], removed: [], addedCount: 1, removedCount: 0 } }
79
106
  ```
80
107
 
@@ -91,21 +118,21 @@ reticle_query({ by: "role", value: "button", name: "Save" }) // → ref + desc
91
118
 
92
119
  ### `reticle_inspect`
93
120
 
94
- Deep detail on one element — including the signals a snapshot/a11y tree omits, so you can tell "present" from "actually usable / on-theme".
121
+ Deep detail on one element, including the signals a snapshot/a11y tree omits, so you can tell "present" from "actually usable / on-theme".
95
122
 
96
123
  - **args:** `ref`, `sessionId?`.
97
- - **returns:** descriptor + `tag` + `box` + `occluded` (another element covers its center — a z-index/overlay bug) + `styles { color, backgroundColor, opacity, cursor, display, visibility }` + `theme { colorToken, backgroundToken, offTheme, tokenCount }` (compliance vs the app's `:root` design tokens — `offTheme:true` flags an off-palette color) + `component { componentStack, source?: { file, line, column } }` (with `@reticlehq/react`).
124
+ - **returns:** descriptor + `tag` + `box` + `occluded` (another element covers its center: a z-index/overlay bug) + `styles { color, backgroundColor, opacity, cursor, display, visibility }` + `theme { colorToken, colorTokens, backgroundToken, backgroundTokens, offTheme, tokenCount, themeScope }` (compliance vs the app's design tokens, where `offTheme:true` flags an off-palette color; the plural fields list every token sharing that colour and the singular ones are `null` when several do, and `themeScope` names the theme active at capture) + `component { componentStack, source?: { file, line, column } }` (with `@reticlehq/react`).
98
125
  - Use it to catch present-but-broken UI: `opacity:0` / `box` 0×0 / `occluded:true` (invisible or unclickable), `cursor` not `pointer` (dead control), `offTheme:true` (off-design-token color).
99
126
 
100
127
  ### `reticle_act` / `reticle_act_sequence`
101
128
 
102
129
  Perform one action / several in order.
103
130
 
104
- - **`reticle_act` args:** `ref`, `action`, `args?`, `refuseWhenThrottled?`, `sessionId?`. → `{ since, dispatched, settled, settleReason, result, session, warning? }` where `result = { ok, ref, action, dispatched, settled, settleReason, effect }`. The `session` block `{ lastSeenMs, throttled, focused }` (F2) reports tab health on every act; when `throttled` is true a `warning` string is also attached. Pass `refuseWhenThrottled: true` to hard-fail instead of warning (opt-in; default is warn-only so background testing never breaks).
131
+ - **`reticle_act` args:** `ref`, `action`, `args?`, `refuseWhenThrottled?`, `sessionId?`. → `{ since, dispatched, settled, settleReason, result, session, warning? }` where `result = { ok, ref, action, dispatched, settled, settleReason, effect }`. The `session` block `{ lastSeenMs, throttled, focused }` reports tab health on every act; when `throttled` is true a `warning` string is also attached. Pass `refuseWhenThrottled: true` to hard-fail instead of warning (opt-in; default is warn-only so background testing never breaks).
105
132
  - **`reticle_act_sequence` args:** `steps: [{ ref, action, args? }]`. → `{ since, dispatched, result }` where `result = { ok, count, effects: [...], steps: [...] }` (one `effect` per step; each step carries its own `dispatched`/`settled`/`settleReason`).
106
- - See [§5](#5-actions--full-list) for the action list.
133
+ - See [§5](#5-actions-full-list) for the action list.
107
134
 
108
- **Dispatch vs settle (F1).** The action is two phases: the **dispatch** (the synchronous click/fill — this is what can fail) and the **settle** (waiting one animation frame so React's commit lands before we return). The settle is **bounded** (~200ms): in a throttled/background tab `requestAnimationFrame` never fires, so Reticle falls back to a timer and resolves anyway. A settle timeout is therefore **never an error** — `reticle_act` resolves with `settled:false, settleReason:"timeout"` and the dispatch (the click) has still landed. Only a real dispatch failure (stale ref, wrong element type) throws.
135
+ **Dispatch vs settle.** The action is two phases: the **dispatch** (the synchronous click/fill, which is what can fail) and the **settle** (waiting one animation frame so React's commit lands before we return). The settle is **bounded** (~200ms): in a throttled/background tab `requestAnimationFrame` never fires, so Reticle falls back to a timer and resolves anyway. A settle timeout is therefore **never an error**: `reticle_act` resolves with `settled:false, settleReason:"timeout"` and the dispatch (the click) has still landed. Only a real dispatch failure (stale ref, wrong element type) throws.
109
136
 
110
137
  | top-level field | meaning |
111
138
  | --- | --- |
@@ -113,7 +140,7 @@ Perform one action / several in order.
113
140
  | `settled` | a real animation frame flushed within the budget; `false` = the fallback timer fired |
114
141
  | `settleReason` | `"timeout"` when the fallback fired (throttled tab), else `null` |
115
142
 
116
- **`result.effect` — best-effort evidence the action landed.** All probes are cheap and capture only the _immediate_ effect (one microtask + one rAF after dispatch); async, network-driven re-renders show up in `reticle_observe`, not here.
143
+ **`result.effect`: best-effort evidence the action landed.** All probes are cheap and capture only the _immediate_ effect (one microtask + one rAF after dispatch); async, network-driven re-renders show up in `reticle_observe`, not here.
117
144
 
118
145
  | field | meaning |
119
146
  | --- | --- |
@@ -125,22 +152,22 @@ Perform one action / several in order.
125
152
  | `focusMoved` | `"<prevRef>-><newRef>"` if `document.activeElement` changed, else `null` (body counts as `null`) |
126
153
  | `valueChanged` | `fill`/`type`/`clear` only: input value before !== after; otherwise `false` |
127
154
  | `domMutatedWithin` | count of MutationObserver records seen in the window |
128
- | `occluded` | `click`/`dblclick` only: the click point hit-tested to a _foreign_ element (an overlay is on top). Synthetic dispatch still delivered the event, but **a real user could not click it** — treat the target as visually blocked. `false` when not click-like or not hit-testable |
155
+ | `occluded` | `click`/`dblclick` only: the click point hit-tested to a _foreign_ element (an overlay is on top). Synthetic dispatch still delivered the event, but **a real user could not click it**, so treat the target as visually blocked. `false` when not click-like or not hit-testable |
129
156
  | `occludedBy` | the ref of the element actually on top at the click point when `occluded`, else `null` |
130
157
  | `scrolledIntoView` | `click`/`dblclick` only: the target was off-viewport, so Reticle scrolled it into view before dispatch |
131
158
 
132
159
  Use it to distinguish failure modes: `visible:false`/`enabled:false`/`targetMatched:false` → your action missed; the tool throwing → it never dispatched; `occluded:true` → the control is covered by something (a real user is blocked even though the synthetic event landed); `defaultPrevented:true` or all of `valueChanged:false`/`focusMoved:null`/`domMutatedWithin:0` → the app didn't react.
133
160
 
134
- **Clicks run the code, they don't push pixels.** A `click`/`dblclick` fires the full `pointerdown → mousedown → focus → pointerup → mouseup → click` sequence directly on the resolved element — so pointer- and focus-gated handlers fire the way they do for a real user, with no coordinate gesture to be intercepted by the presenter HUD or missed off-screen. This is the **default even when native CDP real input is configured** (`inputMode:"synthetic"`, `inputModeReason:"synthetic-click-preferred"`). Before dispatch Reticle hit-tests the click point (`occluded`) and scrolls an off-screen target in (`scrolledIntoView`), so a blocked or off-viewport target is reported, never silently "successful". For the rare case that needs a **trusted** native click — a native file picker, clipboard, or an `isTrusted`-gated handler — pass `args:{ native:true }` to drive it through CDP. `hover`/`drag` still use native pointer input (they need real hit-testing).
161
+ **Clicks run the code, they don't push pixels.** A `click`/`dblclick` fires the full `pointerdown → mousedown → focus → pointerup → mouseup → click` sequence directly on the resolved element, so pointer- and focus-gated handlers fire the way they do for a real user, with no coordinate gesture to be intercepted by the presenter HUD or missed off-screen. This is the **default even when native CDP real input is configured** (`inputMode:"synthetic"`, `inputModeReason:"synthetic-click-preferred"`). Before dispatch Reticle hit-tests the click point (`occluded`) and scrolls an off-screen target in (`scrolledIntoView`), so a blocked or off-viewport target is reported, never silently "successful". For the rare case that needs a **trusted** native click (a native file picker, clipboard, or an `isTrusted`-gated handler), pass `args:{ native:true }` to drive it through CDP. `hover`/`drag` still use native pointer input (they need real hit-testing).
135
162
 
136
- **Cookbook — "Did my action even land?"**
163
+ **Cookbook: "Did my action even land?"**
137
164
 
138
165
  ```ts
139
166
  const { result } = reticle_act({ ref: saveBtn, action: 'click' });
140
167
  if (result.effect.defaultPrevented) {
141
- // a handler blocked the default — the click was swallowed
168
+ // a handler blocked the default, so the click was swallowed
142
169
  } else if (result.effect.domMutatedWithin === 0) {
143
- // dispatched cleanly but the app rendered nothing — likely a dead control
170
+ // dispatched cleanly but the app rendered nothing: likely a dead control
144
171
  }
145
172
  ```
146
173
 
@@ -150,14 +177,14 @@ The timeline + summary of what happened.
150
177
 
151
178
  - **args:** `window_ms?` (default 2000) **or** `since?` (cursor from an act), `filters?` (event-type names), `max_events?` (cap the timeline to the most recent N), `sessionId?`.
152
179
  - **returns:** `{ window_ms, events: [...], summary: { network, domAdded, domRemoved, routeChanges, consoleErrors, animations, signals }, cost: { events, bytes, droppedOldest? } }`.
153
- - **Output budget.** Every result carries a `cost:{ events, bytes }` hint so you can self-budget your next call. When `max_events` truncates the timeline, the dropped count is surfaced as `cost.droppedOldest` — never a silent cap. (The presenter HUD's own animations are filtered out of the timeline automatically, so `observe` shows the app, not the instrument.)
180
+ - **Output budget.** Every result carries a `cost:{ events, bytes }` hint so you can self-budget your next call. When `max_events` truncates the timeline, the dropped count is surfaced as `cost.droppedOldest`, never a silent cap. (The presenter HUD's own animations are filtered out of the timeline automatically, so `observe` shows the app, not the instrument.)
154
181
 
155
182
  ### `reticle_act_and_wait`
156
183
 
157
- Act, then wait for a predicate — the whole act→observe→assert loop in one hop.
184
+ Act, then wait for a predicate: the whole act→observe→assert loop in one hop.
158
185
 
159
186
  - **args:** `ref`, `action`, `args?`, `until: <predicate>`, `timeout_ms?` (default 4000; 0 = evaluate once), `refuseWhenThrottled?`, `sessionId?`.
160
- - **returns:** `{ effect, verdict, trace, session, warning? }` — `effect` is the action result (`{ ok, ref, action }`), `verdict` is `{ pass, evidence?, failureReason? }`, `trace` is the reaction report of everything the app did after the action, and `session` (F2) is the tab-health block `{ lastSeenMs, throttled, focused }` (with a `warning` when throttled). A failing `verdict` still returns `effect` + `trace` so you can see what _did_ happen. The predicate is automatically floored at this act's cursor, so it only matches events the action actually caused.
187
+ - **returns:** `{ effect, verdict, trace, session, warning? }`. `effect` is the action result (`{ ok, ref, action }`), `verdict` is `{ pass, evidence?, failureReason? }`, `trace` is the reaction report of everything the app did after the action, and `session` is the tab-health block `{ lastSeenMs, throttled, focused }` (with a `warning` when throttled). A failing `verdict` still returns `effect` + `trace` so you can see what _did_ happen. The predicate is automatically floored at this act's cursor, so it only matches events the action actually caused.
161
188
 
162
189
  ### `reticle_wait_for`
163
190
 
@@ -173,7 +200,7 @@ Verify a predicate; optionally wait for it.
173
200
  - **args:** `predicate`, `timeout_ms?` (0 = evaluate once), `since?`, `sessionId?`.
174
201
  - Same `since` default as `reticle_wait_for`: scoped to your last act so a stale buffered event can't fake a pass; override with an explicit `since`.
175
202
  - **returns:** `{ verified, because, pass, evidence, contradictions?, coverage?, failureReason?, session, warning? }`. On failure includes a **near-miss** (e.g. "found the dialog but not visible", or "no button named 'Submit'; saw: Cancel"). The `session` block `{ lastSeenMs, throttled, focused }` reports tab health on every assert; when throttled a `warning` is attached so you never assert against a tab that is silently no-oping.
176
- - **Read `verified`, not `pass`.** `pass` says the predicate held; `verified` says whether that means anything. It is `"no"` when a channel contradicts the assertion (a failed write under a green screen, a batch whose body reports per-item failures, a request still in flight), and `"unknown"` when the outcome could not be known yet — a `202 Accepted` that has not reconciled, or a write whose response body was never recorded. `because` names the deciding evidence in one sentence.
203
+ - **Read `verified`, not `pass`.** `pass` says the predicate held; `verified` says whether that means anything. It is `"no"` when a channel contradicts the assertion (a failed write under a green screen, a batch whose body reports per-item failures, a request still in flight), and `"unknown"` when the outcome could not be known yet: a `202 Accepted` that has not reconciled, or a write whose response body was never recorded. `because` names the deciding evidence in one sentence.
177
204
 
178
205
  ### `reticle_reconcile`
179
206
 
@@ -182,13 +209,13 @@ Compare what the API **returned** against what the page **renders**.
182
209
  - **args:** `since?`, `urlContains?`, `sessionId?`.
183
210
  - **returns:** `{ mismatches, compared, note? }`.
184
211
  - Catches the class no status code and no assertion can reach: a `USD 7997` amount rendered as `₹79.97`, or a record the API calls `on_hold` displayed as `"pending"`. Both sides agree on the digits; only the meaning differs, so every other channel reports success.
185
- - Needs response bodies — `connect({ captureNetworkBodies: true })`. When nothing could be compared it says so in `note` rather than returning an empty, clean-looking result over data it never read.
212
+ - Needs response bodies: `connect({ captureNetworkBodies: true })`. When nothing could be compared it says so in `note` rather than returning an empty, clean-looking result over data it never read.
186
213
 
187
214
  ### `reticle_network` / `reticle_console` / `reticle_animations`
188
215
 
189
216
  Fast targeted lookups without a full timeline.
190
217
 
191
- - `reticle_network({ since?, method?, urlContains?, status? })` → `{ calls }`
218
+ - `reticle_network({ since?, method?, urlContains?, status?, bodies? })` → `{ calls }`. Pass `bodies: false` for a body-free listing (method/url/status/timing only); bodies dominate the payload, so the common "did POST /x return 200?" read gets much cheaper.
192
219
  - `reticle_console({ level?, since? })` → `{ logs }`
193
220
  - `reticle_animations()` → running/recent animations.
194
221
 
@@ -202,7 +229,7 @@ The app-advertised testable surface (registered via `reticle.describe`). Call th
202
229
 
203
230
  ### `reticle_domain`
204
231
 
205
- Read the app's domain model **before testing**: a synthesis of every saved flow + the registered capabilities. Tells you what to test and where the real risk is without crawling the app. Reads `.reticle/flows/` + `.reticle/contract.json` — no browser needed.
232
+ Read the app's domain model **before testing**: a synthesis of every saved flow + the registered capabilities. Tells you what to test and where the real risk is without crawling the app. Reads `.reticle/flows/` + `.reticle/contract.json`, no browser needed.
206
233
 
207
234
  - `reticle_domain({})` → `{ flowCount, flows: [{ name, steps, grade, asserts, signals, testids, warning?, risk? }], declared: { testids, signals, stores }, coverage: { asserted, presenceOnly, assertionFree }, gaps: { unassertedFlows, declaredUntestedSignals, declaredUntestedTestids }, riskRanked, summary }`
208
235
  - **`gaps`** is the point: `declaredUntestedSignals` are intents the app emits that **no flow asserts** (untested behavior); `unassertedFlows` act but verify no consequence. Close them with a flow + a consequence assertion (`reticle_annotate`).
@@ -210,7 +237,7 @@ Read the app's domain model **before testing**: a synthesis of every saved flow
210
237
 
211
238
  ### `reticle_state`
212
239
 
213
- Read live framework/store state directly instead of inferring it from the DOM — [§17](#17-evidence-of-effect-actawait-state-capabilities-replay).
240
+ Read live framework/store state directly instead of inferring it from the DOM. See [§17](#17-evidence-of-effect-actawait-state-capabilities-replay).
214
241
 
215
242
  - `reticle_state({ store?, ref?, path?, depth?, sessionId? })` → `{ stores, component? }`, or `{ store, path, found, value, availableKeys?, storeNames }` when `path`/`depth` is given.
216
243
 
@@ -220,14 +247,14 @@ Store reads are the reliable path. The `ref` component read is best-effort and b
220
247
 
221
248
  - `path` extracts a dot-path sub-tree relative to the named `store` (numeric segments index arrays), e.g. `reticle_state({ store:"workspace", path:"captionCache.v3.0.text" })`.
222
249
  - `depth` collapses anything deeper than N levels to a compact size marker (`{…7 keys}`, `[Array(120)]`) so you can skim a store's _shape_ before drilling in.
223
- - A wrong `path` returns `{ found:false, availableKeys:[...] }` — the keys that _were_ present where the walk stopped — so a mistyped path is self-correcting, not a bare `null`.
250
+ - A wrong `path` returns `{ found:false, availableKeys:[...] }`, the keys that _were_ present where the walk stopped, so a mistyped path is self-correcting, not a bare `null`.
224
251
 
225
252
  ### Detecting wasted re-renders (React)
226
253
 
227
- A page can be **thrashing** — committing many React renders a second — while the DOM stays visually identical. The DOM/screenshot tools see an idle page; only a tool inside the runtime sees the commit rate. Reticle exposes it as a registered store you read with `reticle_state`:
254
+ A page can be **thrashing**, committing many React renders a second, while the DOM stays visually identical. The DOM/screenshot tools see an idle page; only a tool inside the runtime sees the commit rate. Reticle exposes it as a registered store you read with `reticle_state`:
228
255
 
229
256
  ```ts
230
- // app entry — MUST run before react-dom loads, so import it FIRST (React reads the devtools hook
257
+ // app entry. MUST run before react-dom loads, so import it FIRST (React reads the devtools hook
231
258
  // at renderer-inject time). It augments a real React DevTools hook if present; host-safe (no-ops on
232
259
  // any failure, never breaks the app).
233
260
  import { installRenderMeter } from '@reticlehq/react';
@@ -239,36 +266,36 @@ reticle_state({ store: "__reticle_renders", path: "commits" }) // → total Re
239
266
  // read it, do an action (or wait a window), read again → the delta is the commit count for that span.
240
267
  ```
241
268
 
242
- A render storm shows up as a commit count that climbs with no corresponding DOM mutation — a perf regression invisible to any outside-the-page tool.
269
+ A render storm shows up as a commit count that climbs with no corresponding DOM mutation: a perf regression invisible to any outside-the-page tool.
243
270
 
244
271
  ### `reticle_session {action:"narrate"}` / `reticle_clock`
245
272
 
246
- Show the agent's intent on the page, and control time (toasts/debounces/auto-dismiss) — [§16](#16-presenter-mode-narration--fake-clock-watch--control).
273
+ Show the agent's intent on the page, and control time (toasts/debounces/auto-dismiss). See [§16](#16-presenter-mode-narration--fake-clock-watch--control).
247
274
 
248
275
  ### `reticle_baseline {action:"save"}` / `reticle_baseline {action:"list"}` / `reticle_baseline {action:"diff"}`
249
276
 
250
- Regression detection — [§8](#8-regression-baselines--diff).
277
+ Regression detection. See [§8](#8-regression-baselines--diff).
251
278
 
252
279
  ### `reticle_record {action:"start"}` / `reticle_record {action:"stop"}` / `reticle_replay`
253
280
 
254
- Capture a flow's reaction report and compile it into a replayable program — [§9](#9-recording-a-flow). `reticle_record {action:"stop"}` also returns a `cost:{ events, bytes }` hint alongside the reaction report so you can gauge the recording's size.
281
+ Capture a flow's reaction report and compile it into a replayable program. See [§9](#9-recording-a-flow). `reticle_record {action:"stop"}` also returns a `cost:{ events, bytes }` hint alongside the reaction report so you can gauge the recording's size.
255
282
 
256
283
  ### `reticle_explore`
257
284
 
258
- List interactive elements + console-error count for autonomous exploration — [§10](#10-autonomous-exploration).
285
+ List interactive elements + console-error count for autonomous exploration. See [§10](#10-autonomous-exploration).
259
286
 
260
287
  ### Flows, recorder & self-healing (`.reticle/`)
261
288
 
262
- `reticle_contract_save`, `reticle_flow_save` / `reticle_flow_save_recorded` / `reticle_flow {action:"list"}` / `reticle_flow {action:"load"}` / `reticle_flow_replay` / `reticle_flow_verify`, `reticle_flow_heal`, `reticle_annotate` — record once, replay forever (anchored on testid/signal — or an auto-derived component/source anchor when there's no testid), with legible drift + self-heal. Full guide: [Flows, the recorder & self-healing](flows.md).
289
+ `reticle_contract_save`, `reticle_flow_save` / `reticle_flow_save_recorded` / `reticle_flow {action:"list"}` / `reticle_flow {action:"load"}` / `reticle_flow_replay` / `reticle_flow_verify`, `reticle_flow_heal`, `reticle_annotate`: record once, replay forever (anchored on testid/signal, or on an auto-derived component/source anchor when there's no testid), with legible drift + self-heal. Full guide: [Flows, the recorder & self-healing](flows.md).
263
290
 
264
- - **`reticle_flow_verify({ names?, sessionId? })`** — the regression-suite call: replays EVERY saved flow (or a subset) deterministically and returns one verdict `{ status, passed, failed, failures: [{ flow, verdict, whatChanged, whereInSource, nextAction }] }`. Passing flows are counted; only failures carry detail. Run it after any change — one call, no LLM per flow.
265
- - **Decision envelope:** on a drift/fail, `reticle_flow_replay` (and each `reticle_flow_verify` failure) returns the actionable fix — `whatChanged`, `whereInSource` (`file:line`), and a one-line `nextAction` (e.g. "rebind the anchor to 'new-deploy', or update the flow if intended").
291
+ - **`reticle_flow_verify({ names?, sessionId? })`** is the regression-suite call: it replays EVERY saved flow (or a subset) deterministically and returns one verdict `{ status, passed, failed, failures: [{ flow, verdict, whatChanged, whereInSource, nextAction }] }`. Passing flows are counted; only failures carry detail. Run it after any change: one call, no LLM per flow.
292
+ - **Decision envelope:** on a drift/fail, `reticle_flow_replay` (and each `reticle_flow_verify` failure) returns the actionable fix: `whatChanged`, `whereInSource` (`file:line`), and a one-line `nextAction` (e.g. "rebind the anchor to 'new-deploy', or update the flow if intended").
266
293
 
267
294
  ### Human-in-the-loop control
268
295
 
269
- `reticle_session {action:"end"}`, `reticle_session {action:"resume"}`, `reticle_session {action:"messages"}` — the human can pause the agent, send it a correction, or end the session from the floating panel; the agent receives guidance on its next tool call. Full guide: [Human-in-the-loop control](human-control.md).
296
+ `reticle_session {action:"end"}`, `reticle_session {action:"resume"}`, `reticle_session {action:"messages"}`: the human can pause the agent, send it a correction, or end the session from the floating panel; the agent receives guidance on its next tool call. Full guide: [Human-in-the-loop control](human-control.md).
270
297
 
271
- ### `reticle_session {action:"review"}` — drain the bugs the human flagged on the page
298
+ ### `reticle_session {action:"review"}`: drain the bugs the human flagged on the page
272
299
 
273
300
  The dev clicks **"Flag a bug"** in the running app, points at the element that looks wrong, and types what's wrong (⌘/Ctrl+Enter to send). Each flag becomes a **mark** the agent drains:
274
301
 
@@ -280,22 +307,22 @@ reticle_session {action:"review"}({ sessionId })
280
307
  pendingCount: 1 }
281
308
  ```
282
309
 
283
- Each pending mark carries the human note, the element label, the source **`file:line`** (when the framework stamped one), and a ready-to-act `fix` hint. Open the file, apply the fix, then `reticle_session {action:"review"}({ resolve: "m1" })` — the human watching the panel sees **"✓ fixed: …"** land. Reading never consumes a mark, so you can list → fix → verify → resolve. `reticle_sessions` also reports `pendingMarks` so you notice flagged bugs during normal orientation.
310
+ Each pending mark carries the human note, the element label, the source **`file:line`** (when the framework stamped one), and a ready-to-act `fix` hint. Open the file, apply the fix, then `reticle_session {action:"review"}({ resolve: "m1" })`. The human watching the panel sees **"✓ fixed: …"** land. Reading never consumes a mark, so you can list → fix → verify → resolve. `reticle_sessions` also reports `pendingMarks` so you notice flagged bugs during normal orientation.
284
311
 
285
- ### `reticle_network_mock` — stub the network for error-state testing (driven mode)
312
+ ### `reticle_network_mock`: stub the network for error-state testing (driven mode)
286
313
 
287
- On a page Reticle drives (`reticle drive`), make a request return a 500, force it offline, or delay it — so testing error/edge states is one declared rule, no backend changes:
314
+ On a page Reticle drives (`reticle drive`), make a request return a 500, force it offline, or delay it, so testing error/edge states is one declared rule, no backend changes:
288
315
 
289
316
  ```
290
317
  reticle_network_mock({ mocks: [{ urlContains: "/api/pay", method: "POST", status: 500 }] })
291
- → { applied: true, count: 1 } // now the checkout POST returns 500 — verify the failure UI
318
+ → { applied: true, count: 1 } // now the checkout POST returns 500; verify the failure UI
292
319
  reticle_network_mock({ mocks: [{ urlContains: "/api/feed", abort: true }] }) // simulate offline
293
320
  reticle_network_mock({ clear: true }) // turn mocking off
294
321
  ```
295
322
 
296
323
  First matching rule wins (`urlContains` + optional case-insensitive `method`). Needs a driven browser; without one it returns a `recommendation` pointing at `reticle drive`.
297
324
 
298
- ### `reticle_viewport` — reproducible visual baselines (driven mode)
325
+ ### `reticle_viewport`: reproducible visual baselines (driven mode)
299
326
 
300
327
  Pin the driven page to a fixed viewport so a screenshot baseline is reproducible across machines:
301
328
 
@@ -304,15 +331,15 @@ reticle_viewport({ width: 1280, height: 800 }) // set once, before reticle_scr
304
331
  → { applied: true, width: 1280, height: 800 }
305
332
  ```
306
333
 
307
- This is one of three knobs for **CI-stable visual regression** — set them together:
334
+ This is one of three knobs for **CI-stable visual regression**. Set them together:
308
335
 
309
- 1. **`reticle_viewport({ width, height })`** — same dimensions on every machine.
310
- 2. **`reticle_clock({ freeze: true })`** — kill animation/time jitter so the pixels are stable.
311
- 3. **`reticle_visual_diff({ baseline, masks: [{ x, y, width, height }] })`** — neutralize volatile regions (clocks, avatars, ids) so only real changes fail.
336
+ 1. **`reticle_viewport({ width, height })`** gives the same dimensions on every machine.
337
+ 2. **`reticle_clock({ freeze: true })`** kills animation/time jitter so the pixels are stable.
338
+ 3. **`reticle_visual_diff({ baseline, masks: [{ x, y, width, height }] })`** neutralizes volatile regions (clocks, avatars, ids) so only real changes fail.
312
339
 
313
340
  ---
314
341
 
315
- ## 4. The predicate DSL — full reference
342
+ ## 4. The predicate DSL: full reference
316
343
 
317
344
  A **predicate** declares what should be true. `reticle_assert` / `reticle_wait_for` evaluate it against the live DOM + the event buffer.
318
345
 
@@ -322,11 +349,14 @@ A **predicate** declares what should be true. `reticle_assert` / `reticle_wait_f
322
349
  // An element exists / is in a state
323
350
  { "kind": "element", "query": { "role": "dialog", "name": "Confirm" }, "state": "visible" }
324
351
  // query supports: role, name, text, label, placeholder, testid, alt, scope
325
- // state: visible | hidden | enabled | disabled | checked | expanded | focused | present
352
+ // state: visible | hidden | enabled | disabled | checked | expanded | focused | present | inViewport
353
+ // inViewport asserts the element is in the viewport NOW (not just in the DOM), so a scrollIntoView is gradeable
326
354
  // add "absent": true to assert it is NOT there (regression / removal)
327
355
 
328
- // Visible text anywhere (optionally scoped via an element query instead)
356
+ // Visible text: page-wide, or `scope` it to a subtree (CSS selector or ref) so a match
357
+ // in a background tab can't satisfy an assertion about the dialog that just opened
329
358
  { "kind": "text", "contains": "Saved successfully", "visible": true }
359
+ { "kind": "text", "contains": "Floor 3", "scope": "[role=dialog]" }
330
360
 
331
361
  // A network call happened
332
362
  { "kind": "net", "method": "POST", "urlContains": "/api/order", "status": 200, "since": 1820 }
@@ -343,21 +373,21 @@ A **predicate** declares what should be true. `reticle_assert` / `reticle_wait_f
343
373
  // An app-emitted signal (webhook/websocket/store change you surfaced via reticle.signal)
344
374
  { "kind": "signal", "name": "webhook:received", "dataMatches": { "provider": "stripe", "id": "*" } }
345
375
 
346
- // A registered store's VALUE — the source of truth no DOM/network read can reach. Walks a dot-path
376
+ // A registered store's VALUE: the source of truth no DOM/network read can reach. Walks a dot-path
347
377
  // (numeric array indices) and matches `equals`: a literal, omitted = presence, or a
348
378
  // { $gte | $lte | $gt | $lt | $contains | $length } operator pattern. Catches a UI-vs-store desync
349
- // (a deploy that only LOOKS shipped) deterministically, in one call — no LLM, no DOM scraping.
379
+ // (a deploy that only LOOKS shipped) deterministically, in one call, with no LLM and no DOM scraping.
350
380
  { "kind": "state", "store": "app", "path": "deployments.0.status", "equals": "live" }
351
381
  ```
352
382
 
353
- A `state` assertion is graded as a **consequence** (a wrong element or stale render cannot fake it), and is usable the same three ways anywhere predicates flow: ad-hoc (`reticle_assert` / `reticle_act_and_wait` `until`), as a flow step invariant (`reticle_annotate { kind: "assert-state", statePath, store?, equals? }`), and as a flow's golden end-condition (`reticle_annotate { kind: "success-state", statePath, … }`). On a miss it names the real store value and the keys that were available — legible, not a blind fail.
383
+ A `state` assertion is graded as a **consequence** (a wrong element or stale render cannot fake it), and is usable the same three ways anywhere predicates flow: ad-hoc (`reticle_assert` / `reticle_act_and_wait` `until`), as a flow step invariant (`reticle_annotate { kind: "assert-state", statePath, store?, equals? }`), and as a flow's golden end-condition (`reticle_annotate { kind: "success-state", statePath, … }`). On a miss it names the real store value and the keys that were available: legible, not a blind fail.
354
384
 
355
385
  ### Combinators
356
386
 
357
387
  ```jsonc
358
- { "allOf": [ <predicate>, <predicate>, … ] } // every one must hold
359
- { "anyOf": [ <predicate>, … ] } // at least one
360
- { "not": <predicate> }
388
+ { "kind": "allOf", "predicates": [ <predicate>, <predicate>, … ] } // every one must hold
389
+ { "kind": "anyOf", "predicates": [ <predicate>, … ] } // at least one
390
+ { "kind": "not", "predicate": <predicate> }
361
391
  ```
362
392
 
363
393
  ### Timing
@@ -369,23 +399,23 @@ A `state` assertion is graded as a **consequence** (a wrong element or stale ren
369
399
 
370
400
  ---
371
401
 
372
- ## 5. Actions — full list
402
+ ## 5. Actions: full list
373
403
 
374
404
  `reticle_act({ ref, action, args })`:
375
405
 
376
406
  | action | args | notes |
377
407
  | --- | --- | --- |
378
- | `click` / `dblclick` | — | dispatches a real click |
379
- | `hover` | — | `mouseover`+`mouseenter` (triggers JS hover state) |
380
- | `focus` / `blur` | — | |
408
+ | `click` / `dblclick` | n/a | dispatches a real click |
409
+ | `hover` | n/a | `mouseover`+`mouseenter` (triggers JS hover state) |
410
+ | `focus` / `blur` | n/a | |
381
411
  | `fill` | `{ value }` | sets value via React-safe native setter + `input`/`change` |
382
412
  | `type` | `{ text }` | appends to current value |
383
- | `clear` | — | empties an input |
413
+ | `clear` | n/a | empties an input |
384
414
  | `select` | `{ value }` | `<select>` option |
385
- | `check` / `uncheck` | — | checkbox/radio |
386
- | `submit` | — | submits the element's `<form>` |
387
- | `press` | `{ key }` | keydown/up (default `Enter`) |
388
- | `scrollIntoView` | — | |
415
+ | `check` / `uncheck` | n/a | checkbox/radio |
416
+ | `submit` | n/a | submits the element's `<form>` |
417
+ | `press` | `{ key, modifiers? }` | keydown/up (default `Enter`); `modifiers`: an array of `Meta` / `Control` / `Shift` / `Alt` for Cmd+K-style shortcuts |
418
+ | `scrollIntoView` | n/a | |
389
419
  | `upload` | `{ name, content?, type? }` | sets a file on `<input type=file>` |
390
420
  | `drag` | `{ toRef }` | pointer-based drag (dnd-kit / rbd) + HTML5 DnD |
391
421
  | `webmcp` | `{ tool, params }` | calls a `navigator.modelContext` tool if the site exposes one |
@@ -394,13 +424,13 @@ A `state` assertion is graded as a **consequence** (a wrong element or stale ren
394
424
 
395
425
  ## 6. Snapshot modes & scoping
396
426
 
397
- `reticle_snapshot` has three modes — pick the cheapest that answers your question:
427
+ `reticle_snapshot` has three modes. Pick the cheapest that answers your question:
398
428
 
399
- - **`status`** (~30 tokens) — route, visible dialogs, counters. "Where am I, is a modal open?"
400
- - **`interactive`** (~100 tokens) — only actionable elements (buttons, inputs, tabs…). "What can I click?" Non-interactive content (e.g. 1,000 list rows) is skipped.
401
- - **`full`** — the whole semantic tree. Use only when you truly need everything.
429
+ - **`status`** (~30 tokens): route, visible dialogs, counters. "Where am I, is a modal open?"
430
+ - **`interactive`** (~100 tokens): only actionable elements (buttons, inputs, tabs…). "What can I click?" Non-interactive content (e.g. 1,000 list rows) is skipped.
431
+ - **`full`**: the whole semantic tree. Use only when you truly need everything.
402
432
 
403
- **`scope`** narrows any snapshot or query to a subtree — a CSS selector (`scope: "[data-testid=item-list]"`) or a ref. This is the main lever for keeping payloads small and queries unambiguous on big pages.
433
+ **`scope`** narrows any snapshot or query to a subtree, either a CSS selector (`scope: "[data-testid=item-list]"`) or a ref. This is the main lever for keeping payloads small and queries unambiguous on big pages.
404
434
 
405
435
  ---
406
436
 
@@ -412,17 +442,17 @@ Each is phrased as the situation you're in, then how the agent verifies it.
412
442
 
413
443
  ```jsonc
414
444
  const { since } = reticle_act({ ref: iconBtn, action: "click" })
415
- reticle_assert({ timeout_ms: 2000, predicate: { allOf: [
445
+ reticle_assert({ timeout_ms: 2000, predicate: { kind: "allOf", predicates: [
416
446
  { kind: "element", query: { role: "dialog" }, state: "visible" },
417
447
  { kind: "console", level: "error", absent: true }
418
448
  ]}})
419
449
  ```
420
450
 
421
- ### "I changed an API call — did it fire correctly and update the UI?"
451
+ ### "I changed an API call: did it fire correctly and update the UI?"
422
452
 
423
453
  ```jsonc
424
454
  const { since } = reticle_act({ ref: saveBtn, action: "click" })
425
- reticle_assert({ timeout_ms: 3000, predicate: { allOf: [
455
+ reticle_assert({ timeout_ms: 3000, predicate: { kind: "allOf", predicates: [
426
456
  { kind: "net", method: "PUT", urlContains: "/api/profile", status: 200, since },
427
457
  { kind: "text", contains: "Saved", visible: true }
428
458
  ]}})
@@ -439,7 +469,7 @@ reticle_assert({ timeout_ms: 2000,
439
469
  predicate: { kind: "text", contains: "New item queued", visible: true } })
440
470
  ```
441
471
 
442
- ### "Data shows up only after ~30s (eventual consistency) — how to refresh and see it"
472
+ ### "Data shows up only after ~30s (eventual consistency): how to refresh and see it"
443
473
 
444
474
  ```jsonc
445
475
  const { since } = reticle_act({ ref: addBtn, action: "click" })
@@ -452,29 +482,29 @@ reticle_wait_for({ timeout_ms: 5000, predicate: { kind: "element",
452
482
  query: { text: name, scope: "[data-testid=item-list]" }, state: "visible" } })
453
483
  ```
454
484
 
455
- ### "The list has 100s/1000s of rows — was my item actually added?"
485
+ ### "The list has 100s/1000s of rows: was my item actually added?"
456
486
 
457
- Don't scroll and eyeball — query finds it regardless of position:
487
+ Don't scroll and eyeball. Query finds it regardless of position:
458
488
 
459
489
  ```jsonc
460
490
  reticle_assert({ timeout_ms: 3000, predicate: { kind: "element",
461
491
  query: { text: "Invoice #4821", scope: "[data-testid=item-list]" }, state: "visible" } })
462
492
  ```
463
493
 
464
- > Note: if your list is **virtualized** (react-window/virtuoso), off-screen rows aren't in the DOM yet — scroll-to-find support is on the roadmap; for now scroll the container or assert against the data via an `reticle.signal`.
494
+ > Note: if your list is **virtualized** (react-window/virtuoso), an off-screen row is not in the DOM at all, so `reticle_query` correctly finds nothing. Use **`reticle_scroll_to`** to scroll the windowed container until the row renders, then query it: `reticle_scroll_to({ by: "text", value: "Invoice #4821", container: "[data-testid=item-list]" })`. Asserting against the data with `reticle.signal` or `reticle_state` also works and is cheaper.
465
495
 
466
- ### "Login form — does it actually authorize?"
496
+ ### "Login form: does it actually authorize?"
467
497
 
468
498
  ```jsonc
469
499
  reticle_act({ ref: emailRef, action: "fill", args: { value: "admin@acme.com" } })
470
500
  reticle_act({ ref: pwRef, action: "fill", args: { value: "•••••••" } })
471
501
  const { since } = reticle_act({ ref: submitRef, action: "click" })
472
- reticle_assert({ timeout_ms: 3000, predicate: { allOf: [
502
+ reticle_assert({ timeout_ms: 3000, predicate: { kind: "allOf", predicates: [
473
503
  { kind: "net", method: "POST", urlContains: "/api/login", status: 200, since },
474
504
  { kind: "element", query: { role: "heading", name: "Dashboard" }, state: "visible" }
475
505
  ]}})
476
506
  // And the failure path:
477
- reticle_assert({ predicate: { allOf: [
507
+ reticle_assert({ predicate: { kind: "allOf", predicates: [
478
508
  { kind: "net", urlContains: "/api/login", status: 401 },
479
509
  { kind: "element", query: { role: "alert" }, state: "visible" }
480
510
  ]}})
@@ -486,11 +516,11 @@ reticle_assert({ predicate: { allOf: [
486
516
  reticle_assert({ predicate: { kind: "console", level: "error", absent: true } })
487
517
  ```
488
518
 
489
- ### "A real LLM call generates a script — is it happening and rendering?"
519
+ ### "A real LLM call generates a script: is it happening and rendering?"
490
520
 
491
521
  ```jsonc
492
522
  const { since } = reticle_act({ ref: generateBtn, action: "click" })
493
- reticle_assert({ timeout_ms: 15000, predicate: { allOf: [
523
+ reticle_assert({ timeout_ms: 15000, predicate: { kind: "allOf", predicates: [
494
524
  { kind: "net", method: "POST", urlContains: "/api/generate", status: 200, since },
495
525
  { kind: "element", query: { testid: "script-output" }, state: "visible" }
496
526
  ]}})
@@ -501,7 +531,7 @@ reticle_assert({ timeout_ms: 15000, predicate: { allOf: [
501
531
  ```jsonc
502
532
  reticle_act({ ref: fileInput, action: "upload", args: { name: "pitch.mp4", type: "video/mp4" } })
503
533
  const { since } = reticle_act({ ref: analyzeBtn, action: "click" })
504
- reticle_assert({ timeout_ms: 15000, predicate: { allOf: [
534
+ reticle_assert({ timeout_ms: 15000, predicate: { kind: "allOf", predicates: [
505
535
  { kind: "net", method: "POST", urlContains: "/api/score", status: 200, since },
506
536
  { kind: "element", query: { role: "dialog", name: "Score result" }, state: "visible" },
507
537
  { kind: "text", contains: "/ 100", visible: true }
@@ -519,7 +549,7 @@ const after = reticle_inspect({ ref }).styles.backgroundColor
519
549
 
520
550
  > Pure CSS `:hover` styling needs a real pointer; drive hover effects from JS state (or use a Playwright real-hover) if you need pixel-exact `:hover`. Reticle reads computed style after the JS state change.
521
551
 
522
- ### "Something off-DOM happened — a webhook arrived, a store changed"
552
+ ### "Something off-DOM happened: a webhook arrived, a store changed"
523
553
 
524
554
  Surface it from your app, then assert on it:
525
555
 
@@ -538,7 +568,7 @@ reticle.describe({
538
568
  });
539
569
  ```
540
570
 
541
- The agent reads this back with `reticle_capabilities()` — see [§3](#3-tool-reference).
571
+ The agent reads this back with `reticle_capabilities()`; see [§3](#3-the-tools-full-reference).
542
572
 
543
573
  ```jsonc
544
574
  reticle_assert({ timeout_ms: 30000, predicate: {
@@ -547,7 +577,7 @@ reticle_assert({ timeout_ms: 30000, predicate: {
547
577
 
548
578
  #### Keeping signals from drifting (lint)
549
579
 
550
- Signals only help if you actually emit one whenever user-visible state changes. The `@reticlehq/eslint-plugin` package ships one rule, `reticle/require-signal-on-mutation`, that flags any function which calls a configured store **mutator** but never fires the **signal callee** in the same body — so the signal map can't silently fall behind the store.
580
+ Signals only help if you actually emit one whenever user-visible state changes. The `@reticlehq/eslint-plugin` package ships `reticle/require-signal-on-mutation`, which flags any function that calls a configured store **mutator** but never fires the **signal callee** in the same body, so the signal map can't silently fall behind the store. (The package also ships `reticle/no-internal-tags`; both are on in its `recommended` config.)
551
581
 
552
582
  ```js
553
583
  // eslint.config.mjs
@@ -599,13 +629,13 @@ reticle_visual_diff({ baseline: "checkout-ok", masks: [{ x: 0, y: 0, width: 200,
599
629
  // → { matched: false, changedPixels, ratio, region, diffPath } // 3. masks ignore volatile regions
600
630
  ```
601
631
 
602
- Without all three, a pixel diff fails on a different window size, a mid-animation frame, or a live clock/avatar — the classic reasons teams give up on screenshot tests. With them, only a real visual change fails.
632
+ Without all three, a pixel diff fails on a different window size, a mid-animation frame, or a live clock/avatar: the classic reasons teams give up on screenshot tests. With them, only a real visual change fails.
603
633
 
604
634
  ---
605
635
 
606
636
  ## 9. Recording a flow
607
637
 
608
- Capture everything that happens across a span — useful for "run my whole checkout flow and tell me what happened," or to keep a known-good trace.
638
+ Capture everything that happens across a span. Useful for "run my whole checkout flow and tell me what happened," or to keep a known-good trace.
609
639
 
610
640
  ```jsonc
611
641
  reticle_record {action:"start"}({ recordingName: "checkout" })
@@ -647,7 +677,7 @@ The agent then acts on each ref, observes the reaction, and reports anomalies (f
647
677
 
648
678
  ## 11. Turning your test cases into agent checks
649
679
 
650
- If you already have test cases — a QA checklist, acceptance criteria, a spreadsheet, manual steps — you can hand them to your agent and have it run + verify each against the live app. Each case becomes a predicate:
680
+ If you already have test cases (a QA checklist, acceptance criteria, a spreadsheet, manual steps), you can hand them to your agent and have it run + verify each against the live app. Each case becomes a predicate:
651
681
 
652
682
  | Test case (English) | Reticle check |
653
683
  | --- | --- |
@@ -662,7 +692,7 @@ A practical workflow:
662
692
 
663
693
  > "Here are our 12 dashboard test cases. For each, drive the app with Reticle and tell me pass/fail with evidence. For any failure, show the source file to fix."
664
694
 
665
- This is the sweet spot: the **manual cases you never automated** become things the agent runs in seconds, on every change. It **complements** your CI Playwright/Cypress suite (which gates releases) — Reticle is the in-loop checklist while you build.
695
+ This is the sweet spot: the **manual cases you never automated** become things the agent runs in seconds, on every change. It **complements** your CI Playwright/Cypress suite (which gates releases); Reticle is the in-loop checklist while you build.
666
696
 
667
697
  ---
668
698
 
@@ -680,10 +710,10 @@ Reticle is cheap by design ([benchmark](token-efficiency.md)), but keep it that
680
710
  ## 13. Best practices & gotchas
681
711
 
682
712
  - **Accessibility = legibility.** Real `role`s, labels, and `data-testid`s make queries precise and stable. It's also just good a11y.
683
- - **Stable handles for controls.** Prefer `data-testid` over names that include dynamic counts (e.g. "Notifications (3)") — the count changes the accessible name.
713
+ - **Stable handles for controls.** Prefer `data-testid` over names that include dynamic counts (e.g. "Notifications (3)"); the count changes the accessible name.
684
714
  - **Always thread `since`.** Pass the cursor from `reticle_act` into `observe`/`assert` so you only consider what happened _after_ the action.
685
715
  - **Use `timeout_ms` for async.** Don't assert instantly on something that arrives over the network or after a re-render.
686
- - **Watch `session.throttled` (F2).** Background tabs throttle timers/rAF/pointer gestures, so an act can silently no-op. Every `reticle_act` / `reticle_assert` / `reticle_act_and_wait` result carries `session: { lastSeenMs, throttled, focused }` and, when throttled, a `warning`. Refocus the tab (or run it foregrounded) before driving; pass `refuseWhenThrottled: true` to hard-fail instead.
716
+ - **Watch `session.throttled`.** Background tabs throttle timers/rAF/pointer gestures, so an act can silently no-op. Every `reticle_act` / `reticle_assert` / `reticle_act_and_wait` result carries `session: { lastSeenMs, throttled, focused }` and, when throttled, a `warning`. Refocus the tab (or run it foregrounded) before driving; pass `refuseWhenThrottled: true` to hard-fail instead.
687
717
  - **Scope big pages.** On dashboards with hundreds of elements, scope queries to the panel you care about.
688
718
  - **Never breaks your app.** Observers are additive and reversible (`reticle.disconnect()` restores patched globals). It won't interfere with your app's behavior.
689
719
 
@@ -691,32 +721,46 @@ Reticle is cheap by design ([benchmark](token-efficiency.md)), but keep it that
691
721
 
692
722
  ## 14. FAQ
693
723
 
694
- **Does this run in production?** No — keep `reticle.connect()` behind a dev guard. The SDK is side-effect-free and tree-shakes out of prod builds.
724
+ ### Does this run in production?
725
+
726
+ No. Keep `reticle.connect()` behind a dev guard. The SDK is side-effect-free and tree-shakes out of prod builds.
727
+
728
+ ### Do I have to change my components?
729
+
730
+ No, for basic look/act/observe. You'll get better results by adding `data-testid`s and labels where the agent needs precision.
731
+
732
+ ### Does it work without React?
733
+
734
+ The core (DOM/network/route/console/animation/snapshot/actions) is framework-agnostic and is gated against a vanilla-TS app. React, Next.js, Remix and Astro each have an app and a CI gate. SvelteKit is wired end-to-end. `reticle init` writes the client hook and the Vite plugin, and the plugin stamps `data-reticle-source` into `.svelte` components so verdicts carry `file:line`. But there is still no SvelteKit app in CI, so it is unverified rather than supported. Vue has a Pinia store adapter and nothing else: no detection, no `.vue` stamping, no gate. See [what Svelte support is and is not](getting-started.md#what-svelte-support-is-and-what-it-is-not).
735
+
736
+ ### Can it judge whether my UI looks good?
737
+
738
+ No. Reticle verifies behavior, not aesthetics. Visual/pixel correctness and "does it feel right" remain human (or a visual-diff tool).
695
739
 
696
- **Do I have to change my components?** No, for basic look/act/observe. You'll get better results by adding `data-testid`s and labels where the agent needs precision.
740
+ ### Does it replace Playwright/Cypress?
697
741
 
698
- **Does it work without React?** The core (DOM/network/route/console/animation/snapshot/actions) is framework-agnostic and is gated against a vanilla-TS app. React, Next.js, Remix and Astro each have an app and a CI gate. SvelteKit is wired end-to-end — `reticle init` writes the client hook and the Vite plugin, and the plugin stamps `data-reticle-source` into `.svelte` components so verdicts carry `file:line` — but there is still no SvelteKit app in CI, so it is unverified rather than supported. Vue has a Pinia store adapter and nothing else: no detection, no `.vue` stamping, no gate. See [what Svelte support is and is not](getting-started.md#what-svelte-support-is-and-what-it-is-not).
742
+ No. Those are your scripted CI suite. Reticle is for in-loop verification while the agent codes, and for the cases you never automated. They compose.
699
743
 
700
- **Can it judge whether my UI _looks_ good?** No. Reticle verifies behavior, not aesthetics. Visual/pixel correctness and "does it feel right" remain human (or a visual-diff tool).
744
+ ### How does it compare to Playwright MCP / Chrome DevTools MCP?
701
745
 
702
- **Does it replace Playwright/Cypress?** No — those are your scripted CI suite. Reticle is for in-loop verification while the agent codes, and for the cases you never automated. They compose.
746
+ Those let an agent drive/ inspect a _separate_ browser; Reticle verifies your _own running app_ (real session/auth) with assertions + regression as first-class, far more cheaply. See [Reticle vs Playwright MCP](vs-playwright-mcp.mdx) and [Reticle vs Chrome DevTools MCP](vs-chrome-devtools-mcp.mdx).
703
747
 
704
- **How does it compare to Playwright MCP / Chrome DevTools MCP?** Those let an agent drive/ inspect a _separate_ browser; Reticle verifies your _own running app_ (real session/auth) with assertions + regression as first-class, far more cheaply. See the README comparison.
748
+ ### Multiple tabs/apps?
705
749
 
706
- **Multiple tabs/apps?** Each is a session; pass `sessionId` to any tool when more than one is connected (`reticle_sessions` lists them).
750
+ Each is a session; pass `sessionId` to any tool when more than one is connected (`reticle_sessions` lists them).
707
751
 
708
752
  ---
709
753
 
710
754
  ## 15. Security & privacy
711
755
 
712
756
  - **Dev-only, localhost-only by default.** The bridge binds `127.0.0.1`; the SDK is meant for dev builds.
713
- - **No app data leaves your machine.** Baselines/recordings are local. The CLI sends anonymous, opt-out usage metrics only (random id + event names — no code, no PII; see [telemetry](telemetry.md)); opt out with `reticle telemetry disable`, `RETICLE_TELEMETRY=0`, or `DO_NOT_TRACK=1`. Feedback you or your agent deliberately send (`reticle feedback` / `reticle_feedback`) is the only free text that ever leaves the machine — never passive, redacted first, and separately disabled with `RETICLE_FEEDBACK=0`.
714
- - **Network bodies aren't captured by default** — only method/url/status/timing. Body capture is opt-in and runs through a redactor (drop `password`/`token`/`secret`/… + your patterns).
757
+ - **No app data leaves your machine.** Baselines/recordings are local. The CLI sends anonymous, opt-out usage metrics only (random id + event names, no code and no PII; see [telemetry](telemetry.md)); opt out with `reticle telemetry disable`, `RETICLE_TELEMETRY=0`, or `DO_NOT_TRACK=1`. Feedback you or your agent deliberately send (`reticle feedback` / `reticle_feedback`) is the only free text that ever leaves the machine: never passive, redacted first, and separately disabled with `RETICLE_FEEDBACK=0`.
758
+ - **Network bodies aren't captured by default:** only method/url/status/timing. Body capture is opt-in and runs through a redactor (drop `password`/`token`/`secret`/… + your patterns).
715
759
  - **Additive & reversible.** Reticle patches `fetch`/History/console defensively and restores them on disconnect; it will not break the app under test.
716
760
 
717
761
  ### Extending the redaction rules
718
762
 
719
- The built-in rule catches the credential names that are common across apps. Yours has its own vocabulary in both directions — a `licenceKey` it has never heard of, and a `designToken` it redacts by mistake — so `connect()` takes a `redact` option:
763
+ The built-in rule catches the credential names that are common across apps. Yours has its own vocabulary in both directions (a `licenceKey` it has never heard of, and a `designToken` it redacts by mistake), so `connect()` takes a `redact` option:
720
764
 
721
765
  ```ts
722
766
  reticle.connect({
@@ -727,17 +771,17 @@ reticle.connect({
727
771
  });
728
772
  ```
729
773
 
730
- - **`keys`** adds to the rule. A string matches a key name **exactly**, case-insensitively — `'code'` does not redact `codeOwner`. A RegExp is tested against the key.
731
- - **`allow`** exempts a key from the **default** rule. It loses to `keys`: an explicit redact instruction beats an exemption. Exempting a key the default rule considers a credential prints a one-time warning naming it — that value now reaches the agent transcript and the on-disk journal in cleartext.
774
+ - **`keys`** adds to the rule. A string matches a key name **exactly**, case-insensitively, so `'code'` does not redact `codeOwner`. A RegExp is tested against the key.
775
+ - **`allow`** exempts a key from the **default** rule. It loses to `keys`: an explicit redact instruction beats an exemption. Exempting a key the default rule considers a credential prints a one-time warning naming it, because that value now reaches the agent transcript and the on-disk journal in cleartext.
732
776
  - **There is no way to replace the default set.** Both options are additive on purpose: a config that could turn the whole rule off would eventually ship in an app that leaks, and Reticle would be the thing that recorded it.
733
- - **With no `redact` option, behaviour is exactly what it was before this option existed** — pinned by a test that walks every credential name and every known false positive.
777
+ - **With no `redact` option, behaviour is exactly what it was before this option existed**, pinned by a test that walks every credential name and every known false positive.
734
778
 
735
- **What crosses the bridge, and why it matters.** Most captures pass through the SDK in your page. Request bodies and response headers on the **driven** path (`reticle drive`, or a CDP-attached browser) do not — the daemon reads them straight from the network stack. So the literal strings in `keys` are announced to the daemon when your app connects, and it redacts them there too. Two parts deliberately stay in the page:
779
+ **What crosses the bridge, and why it matters.** Most captures pass through the SDK in your page. Request bodies and response headers on the **driven** path (`reticle drive`, or a CDP-attached browser) do not: the daemon reads them straight from the network stack. So the literal strings in `keys` are announced to the daemon when your app connects, and it redacts them there too. Two parts deliberately stay in the page:
736
780
 
737
781
  | | Applies in the page | Applies on the driven path |
738
782
  | ----------------------- | :-----------------: | :------------------------: |
739
- | `keys` — plain strings | ✅ | ✅ |
740
- | `keys` — RegExp entries | ✅ | ❌ |
783
+ | `keys` (plain strings) | ✅ | ✅ |
784
+ | `keys` (RegExp entries) | ✅ | ❌ |
741
785
  | `allow` | ✅ | ❌ |
742
786
 
743
787
  A RegExp does not travel because compiling a pattern that arrived over a socket and running it against every key of every request body is a denial-of-service surface. `allow` does not travel because it is the only part of the config that **removes** redaction, and the driven path keeps the built-in floor rather than letting a page lower it. Both exclusions fail in the safe direction: the driven path can over-redact relative to your config, never under. **If a key must be redacted everywhere, name it as a plain string.**
@@ -746,7 +790,7 @@ A RegExp does not travel because compiling a pattern that arrived over a socket
746
790
 
747
791
  ## 16. Presenter mode, narration & fake clock (watch + control)
748
792
 
749
- ### Presenter mode — let a human watch the agent
793
+ ### Presenter mode: let a human watch the agent
750
794
 
751
795
  Turn it on when connecting:
752
796
 
@@ -763,18 +807,18 @@ You get, in the page itself:
763
807
 
764
808
  All presenter DOM uses `data-reticle-*` and is excluded from snapshots/observers, so it never pollutes what the agent sees. Use `setIgnoreSelectors([...])` to also hide your own dev widgets.
765
809
 
766
- #### Session liveness — the HUD never gets stuck "running"
810
+ #### Session liveness: the HUD never gets stuck "running"
767
811
 
768
- A session starts on the agent's first activity and must reliably end even when the agent misbehaves. Reticle is an MCP tool, so the agent (Claude) can crash, disconnect, or simply forget to call `reticle_session {action:"end"}` — and a backgrounded tab's own timers are throttled by the browser. So **the Node server owns liveness, not the browser tab:**
812
+ A session starts on the agent's first activity and must reliably end even when the agent misbehaves. Reticle is an MCP tool, so the agent (Claude) can crash, disconnect, or simply forget to call `reticle_session {action:"end"}`, and a backgrounded tab's own timers are throttled by the browser. So **the Node server owns liveness, not the browser tab:**
769
813
 
770
814
  - **Agent goes idle / forgets to end** → a server-side reaper (immune to tab throttling) ends the session after `idleEndMs` of no agent commands and pushes the end to the browser. A backgrounded tab still receives that push, so you can switch windows and come back to a correctly-ended HUD.
771
815
  - **Agent (MCP client) disconnects cleanly** → every active session ends at once.
772
816
  - **Agent kills the Reticle server process** (so no push can arrive) → the SDK self-ends the session after it can't reach the bridge for `BRIDGE_LOST_MS` (~15s), showing "lost connection to Reticle."
773
817
  - **Slow-but-alive agent** → if it goes quiet long enough to auto-end and then acts again, the session **revives** automatically (an explicit `reticle_session {action:"end"}` stays terminal).
774
818
 
775
- Tune the idle window with `reticle_session({ idleEndMs })` — it updates both the browser timer and the server reaper. The human keeps the panel (with Copy/Export of the run) after any end.
819
+ Tune the idle window with `reticle_session({ action: "tune", idleEndMs })`; it updates both the browser timer and the server reaper. The human keeps the panel (with Copy/Export of the run) after any end.
776
820
 
777
- ### `reticle_session {action:"narrate"}` — show the agent's intent
821
+ ### `reticle_session {action:"narrate"}`: show the agent's intent
778
822
 
779
823
  So the human sees _what the agent is about to do and why_:
780
824
 
@@ -782,16 +826,16 @@ So the human sees _what the agent is about to do and why_:
782
826
  reticle_session {action:"narrate"}({ text: "Adding a beat, then checking the section count goes up" })
783
827
  ```
784
828
 
785
- It renders on the HUD. (The agent's private reasoning isn't visible to Reticle — narration is how it surfaces intent on the page.)
829
+ It renders on the HUD. (The agent's private reasoning isn't visible to Reticle; narration is how it surfaces intent on the page.)
786
830
 
787
- ### `reticle_clock` — control time deterministically
831
+ ### `reticle_clock`: control time deterministically
788
832
 
789
833
  Fast-forward toasts, debounces, auto-dismiss, and commit-on-blur without waiting:
790
834
 
791
835
  ```jsonc
792
836
  reticle_clock({ freeze: true }) // freeze app timers (Date.now/setTimeout/setInterval)
793
837
  reticle_act({ ref: e9, action: "click" })
794
- reticle_clock({ advanceMs: 5000 }) // jump 5s — the auto-dismiss fires now, deterministically
838
+ reticle_clock({ advanceMs: 5000 }) // jump 5s: the auto-dismiss fires now, deterministically
795
839
  reticle_assert({ predicate: { kind: "element", query: { role: "alert" }, absent: true } })
796
840
  reticle_clock({ reset: true }) // restore real timers
797
841
  ```
@@ -800,7 +844,7 @@ It does **not** freeze `requestAnimationFrame`/microtasks (React's scheduler kee
800
844
 
801
845
  ### Action refinements (from real-app use)
802
846
 
803
- - **`blur`** now fires a bubbling `focusout`, so React's commit-on-blur (`onBlur`) runs — inline editors and form fields commit. `fill`/`type` focus first so a later `blur` commits.
847
+ - **`blur`** now fires a bubbling `focusout`, so React's commit-on-blur (`onBlur`) runs, and inline editors and form fields commit. `fill`/`type` focus first so a later `blur` commits.
804
848
  - **`hover`** accepts `{ holdMs }` to dwell, so timer-gated reveals mount; then `wait_for` the revealed nodes.
805
849
  - **`drag`** yields a frame between phases (React flushes between steps) and accepts `{ data: { mime, value } }` for custom `dataTransfer` payloads.
806
850
 
@@ -821,7 +865,7 @@ On a failed signal assert, the result includes a **near-miss**: the signals that
821
865
 
822
866
  ## 17. Evidence-of-effect, act+await, state, capabilities, replay
823
867
 
824
- These close the "is the action trusted?" gap — so you can tell _my action missed_ vs _the app didn't react_ vs _the tool didn't dispatch_.
868
+ These close the "is the action trusted?" gap, so you can tell _my action missed_ vs _the app didn't react_ vs _the tool didn't dispatch_.
825
869
 
826
870
  ### `reticle_act` returns evidence-of-effect
827
871
 
@@ -834,11 +878,11 @@ Every `reticle_act` result now carries an `effect`:
834
878
  focusMoved: "e11->e12"|null, valueChanged, domMutatedWithin } } }
835
879
  ```
836
880
 
837
- `settled:false, settleReason:"timeout"` means the settle frame did not flush within the budget (a throttled/background tab) — this is **not** a failure: the dispatch landed and the tool resolved.
881
+ `settled:false, settleReason:"timeout"` means the settle frame did not flush within the budget (a throttled/background tab). This is **not** a failure: the dispatch landed and the tool resolved.
838
882
 
839
883
  Read it to disambiguate failures instantly: `targetMatched:false` = your ref was stale; `defaultPrevented:true` = a handler cancelled it; `domMutatedWithin:0` + `valueChanged:false` = the app didn't react.
840
884
 
841
- ### `reticle_act_and_wait` — one hop for act → observe → assert
885
+ ### `reticle_act_and_wait`: one hop for act → observe → assert
842
886
 
843
887
  ```jsonc
844
888
  reticle_act_and_wait({ ref, action, args?, until: <predicate>, timeout_ms })
@@ -847,7 +891,7 @@ reticle_act_and_wait({ ref, action, args?, until: <predicate>, timeout_ms })
847
891
 
848
892
  Performs the action (with settle so React commits land in the window), waits for `until`, and returns the action's effect + the verdict + the full causal trace. Collapses four calls into one.
849
893
 
850
- ### `reticle_state` — read live framework/store state
894
+ ### `reticle_state`: read live framework/store state
851
895
 
852
896
  No need to broadcast a signal for every fact. Register stores in your app:
853
897
 
@@ -856,7 +900,7 @@ import { registerStore } from '@reticlehq/react';
856
900
  registerStore('workspace', useWorkspace); // pass the store itself → auto STATE_CHANGE diffs
857
901
  ```
858
902
 
859
- **Which state libraries work.** `registerStore` accepts anything shaped `{ getState, subscribe }`, so **zustand and Redux (and Redux Toolkit) need no adapter at all** — pass the store. For everything else Reticle ships adapters, because the shape is the only thing missing:
903
+ **Which state libraries work.** `registerStore` accepts anything shaped `{ getState, subscribe }`, so **zustand and Redux (and Redux Toolkit) need no adapter at all**: pass the store. For everything else Reticle ships adapters, because the shape is the only thing missing:
860
904
 
861
905
  ```ts
862
906
  import {
@@ -875,15 +919,15 @@ registerStore('app', jotaiStore(getDefaultStore(), { cart, user })); // Jotai (n
875
919
  registerStore('machine', xstateStore(actor)); // XState
876
920
  registerStore('app', valtioStore(state, snapshot, subscribe)); // Valtio
877
921
  registerStore('app', mobxStore(store, toJS, reaction)); // MobX
878
- registerStore('cart', svelteStore(cartStore)); // Svelte store — `{ subscribe }` is the whole contract
922
+ registerStore('cart', svelteStore(cartStore)); // Svelte store: `{ subscribe }` is the whole contract
879
923
  registerStore('cart', piniaStore(useCartStore())); // Pinia (Vue)
880
924
  ```
881
925
 
882
926
  **Svelte and Pinia** are the two whose adapters do something you would not guess from the shape.
883
927
 
884
- A Svelte store has **no pull side at all** — `{ subscribe }` is the entire contract, no `getState`. `svelteStore` reads the current value by subscribing, catching the synchronous first callback the store contract guarantees, and unsubscribing immediately (the same thing `svelte/store`'s own `get()` does), so it holds no lasting subscription and needs no teardown. It also _swallows_ that first callback on `subscribe`, because forwarding it would emit a state change at registration time for a change that never happened.
928
+ A Svelte store has **no pull side at all**: `{ subscribe }` is the entire contract, no `getState`. `svelteStore` reads the current value by subscribing, catching the synchronous first callback the store contract guarantees, and unsubscribing immediately (the same thing `svelte/store`'s own `get()` does), so it holds no lasting subscription and needs no teardown. It also _swallows_ that first callback on `subscribe`, because forwarding it would emit a state change at registration time for a change that never happened.
885
929
 
886
- `piniaStore` subscribes with `detached: true` and `flush: 'sync'`. Without `detached`, a store registered from inside a component goes permanently silent after that component unmounts — still readable, but never emitting another state change, which reads exactly like an app that stopped changing. Without `sync`, the notification lands a Vue tick late, outside the window that links a state change to the click that caused it. Note that `$state` carries state, not getters: a Pinia getter is derived, so asserting on the state it derives from is the stronger assertion anyway.
930
+ `piniaStore` subscribes with `detached: true` and `flush: 'sync'`. Without `detached`, a store registered from inside a component goes permanently silent after that component unmounts: still readable, but never emitting another state change, which reads exactly like an app that stopped changing. Without `sync`, the notification lands a Vue tick late, outside the window that links a state change to the click that caused it. Note that `$state` carries state, not getters: a Pinia getter is derived, so asserting on the state it derives from is the stronger assertion anyway.
887
931
 
888
932
  **Recoil** has no enumerable registry of live atoms and no per-atom subscription outside React, so it takes an atom map (like Jotai) plus the transaction stream from a small bridge component:
889
933
 
@@ -916,16 +960,16 @@ function ReticleRecoilBridge() {
916
960
 
917
961
  Each atom comes back as `{ status, value, error }` rather than a bare value, because Recoil atoms can be async: calling `getValue()` on a pending selector **throws the pending promise**, which would lose the whole state read over one slow atom. A loading atom reports `status: 'loading'` instead of silently reading as empty.
918
962
 
919
- **TanStack Query is worth registering even if you register nothing else.** Its cache holds the state most likely to be wrong in a way nothing else can observe: a stale value served as fresh, a mutation that never invalidated its query, an optimistic update never rolled back. None of those fire a network request, so a network log shows silence and the DOM shows a plausible number — the cache is the only witness. The adapter exposes `status`, `fetchStatus`, `isStale` and `dataUpdatedAt` per query key, so an agent can assert the stronger property: not "the number rendered is 42" but "the number rendered came from fresh data".
963
+ **TanStack Query is worth registering even if you register nothing else.** Its cache holds the state most likely to be wrong in a way nothing else can observe: a stale value served as fresh, a mutation that never invalidated its query, an optimistic update never rolled back. None of those fire a network request, so a network log shows silence and the DOM shows a plausible number. The cache is the only witness. The adapter exposes `status`, `fetchStatus`, `isStale` and `dataUpdatedAt` per query key, so an agent can assert the stronger property: not "the number rendered is 42" but "the number rendered came from fresh data".
920
964
 
921
- **React Context / `useState` / `useReducer`** have no store object to adapt — the value lives in the fiber tree and the only subscription is a re-render. Invert it with the hook:
965
+ **React Context / `useState` / `useReducer`** have no store object to adapt: the value lives in the fiber tree and the only subscription is a re-render. Invert it with the hook:
922
966
 
923
967
  ```tsx
924
968
  import { useReticleStore } from '@reticlehq/react/store';
925
969
 
926
970
  function CartProvider({ children }) {
927
971
  const [cart, dispatch] = useReducer(cartReducer, initial);
928
- useReticleStore('cart', cart); // one line — the agent can now read and assert on cart
972
+ useReticleStore('cart', cart); // one line, and the agent can now read and assert on cart
929
973
  return <CartContext.Provider value={cart}>{children}</CartContext.Provider>;
930
974
  }
931
975
  ```
@@ -933,8 +977,8 @@ function CartProvider({ children }) {
933
977
  ```jsonc
934
978
  reticle_state({ store: "workspace" }) // → { stores: { workspace: {…} } }
935
979
  reticle_state({ ref: "e9" }) // → { component: { ok: true, component, hooks } } or { component: { ok: false, reason: "component-state-unavailable" } }
936
- // `hooks` carries the hook VALUES only (state / ref / memo). React effect entries — chained,
937
- // null-filled fiber internals with nothing to act on — are dropped, and when any were, the read
980
+ // `hooks` carries the hook VALUES only (state / ref / memo). React effect entries (chained,
981
+ // null-filled fiber internals with nothing to act on) are dropped, and when any were, the read
938
982
  // says so: component.truncation = { droppedItems, note }.
939
983
 
940
984
  // Scope a large store instead of paying for the whole thing:
@@ -943,11 +987,11 @@ reticle_state({ store: "workspace", depth: 1 }) // → top-level
943
987
  reticle_state({ store: "workspace", path: "nope" }) // → { found: false, availableKeys: ["captionCache", "version", …] }
944
988
  ```
945
989
 
946
- Store reads are the reliable path; ref reads degrade to a structured failure rather than blocking. `path` (dot-path, numeric segments index arrays) and `depth` keep a 60KB store from becoming a token tax — and a wrong `path` returns the keys that _were_ there, so it's self-correcting.
990
+ Store reads are the reliable path; ref reads degrade to a structured failure rather than blocking. `path` (dot-path, numeric segments index arrays) and `depth` keep a 60KB store from becoming a token tax, and a wrong `path` returns the keys that _were_ there, so it's self-correcting.
947
991
 
948
- ### Charts and dashboards — geometry faults report themselves
992
+ ### Charts and dashboards: geometry faults report themselves
949
993
 
950
- Every dashboard widget except one renders text a comparison can read: a KPI card renders a number, a table renders rows. A **chart renders geometry** — the data has been through a scale function into coordinates — and a perfectly correct `series` in the store can still become a blank, flat, or NaN-filled path. Neither a state read nor a screenshot-vs-baseline catches that.
994
+ Every dashboard widget except one renders text a comparison can read: a KPI card renders a number, a table renders rows. A **chart renders geometry** (the data has been through a scale function into coordinates), and a perfectly correct `series` in the store can still become a blank, flat, or NaN-filled path. Neither a state read nor a screenshot-vs-baseline catches that.
951
995
 
952
996
  So any element descriptor containing faulty plot geometry carries a `chart` field. There is no extra tool call and no flag: query the chart the way you already would, and a broken one tells you.
953
997
 
@@ -958,7 +1002,7 @@ reticle_query({ by: "testid", value: "revenue-chart" })
958
1002
  // sample: "0,10 5,NaN 10,20" }] }] }
959
1003
  ```
960
1004
 
961
- `kind` is one of `non-finite-coordinates` (a zero-range scale divided by zero — always a bug), `empty-geometry` (the chart mounted but no data reached it), or `degenerate-geometry` (every point identical). A **healthy chart adds no field at all**, so this costs nothing on the common path. A genuinely flat line — constant data — is not flagged.
1005
+ `kind` is one of `non-finite-coordinates` (a zero-range scale divided by zero, always a bug), `empty-geometry` (the chart mounted but no data reached it), or `degenerate-geometry` (every point identical). A **healthy chart adds no field at all**, so this costs nothing on the common path. A genuinely flat line (constant data) is not flagged.
962
1006
 
963
1007
  **Canvas charts** (Chart.js, ECharts) are pixels, not DOM, so nothing above applies. Read their data directly instead, which is the only route short of vision:
964
1008
 
@@ -967,7 +1011,7 @@ import { canvasChartData } from '@reticlehq/browser';
967
1011
  canvasChartData(canvasEl, window); // → { library: "chartjs", data: { datasets: [...] } }
968
1012
  ```
969
1013
 
970
- ### `reticle_capabilities` — the app's testable surface
1014
+ ### `reticle_capabilities`: the app's testable surface
971
1015
 
972
1016
  Declare it once so the agent learns the surface without reading source:
973
1017
 
@@ -980,40 +1024,40 @@ registerCapabilities({ testids: [...], signals: [...], stores: [...], flows: [..
980
1024
  reticle_capabilities() // → { testids, signals, stores, flows }
981
1025
  ```
982
1026
 
983
- ### `reticle_replay` — recordings become re-runnable programs
1027
+ ### `reticle_replay`: recordings become re-runnable programs
984
1028
 
985
- `reticle_record {action:"start"}` → drive the flow → `reticle_record {action:"stop"}` returns a **compiled program** (steps bound to testids/signals, not volatile refs). `reticle_replay({ recordingName })` re-executes it — your flow becomes a deterministic regression run, not a checklist.
1029
+ `reticle_record {action:"start"}` → drive the flow → `reticle_record {action:"stop"}` returns a **compiled program** (steps bound to testids/signals, not volatile refs). `reticle_replay({ recordingName })` re-executes it, so your flow becomes a deterministic regression run, not a checklist.
986
1030
 
987
1031
  ---
988
1032
 
989
- ## 18. Real input mode — native hover & drag
1033
+ ## 18. Real input mode: native hover & drag
990
1034
 
991
- Reticle drives actions by dispatching JS events from inside the page. That covers click, fill, type, select, submit, press, and HTML5 drag — but it **cannot** trigger browser-native pointer behavior: `onMouseEnter`/`onMouseLeave`, hover-gated reveals, and pointer-library drags rely on the browser's real hit-testing, which synthetic events don't drive.
1035
+ Reticle drives actions by dispatching JS events from inside the page. That covers click, fill, type, select, submit, press, and HTML5 drag, but it **cannot** trigger browser-native pointer behavior: `onMouseEnter`/`onMouseLeave`, hover-gated reveals, and pointer-library drags rely on the browser's real hit-testing, which synthetic events don't drive.
992
1036
 
993
- **Clicks are synthetic by default — on purpose.** Even with real input configured, `click`/`dblclick` run the occlusion-honest synthetic path (full `pointerdown→…→click` sequence + a `occluded` hit-test + off-viewport auto-scroll), reporting `inputModeReason:"synthetic-click-preferred"`. There's no coordinate gesture for the presenter HUD to intercept or to miss off-screen, and synthetic dispatch reaches the resolved element directly. Reserve native clicks for the rare `isTrusted`-gated case (native file picker, clipboard) with `args:{ native:true }`. Real input remains the path for `hover`/`drag`, which genuinely need the browser's hit-testing. Every `reticle_act` result tells you which path ran:
1037
+ **Clicks are synthetic by default, on purpose.** Even with real input configured, `click`/`dblclick` run the occlusion-honest synthetic path (full `pointerdown→…→click` sequence + a `occluded` hit-test + off-viewport auto-scroll), reporting `inputModeReason:"synthetic-click-preferred"`. There's no coordinate gesture for the presenter HUD to intercept or to miss off-screen, and synthetic dispatch reaches the resolved element directly. Reserve native clicks for the rare `isTrusted`-gated case (native file picker, clipboard) with `args:{ native:true }`. Real input remains the path for `hover`/`drag`, which genuinely need the browser's hit-testing. Every `reticle_act` result tells you which path ran:
994
1038
 
995
1039
  ```jsonc
996
1040
  { since, dispatched, settled, inputMode: "synthetic" | "real", inputModeReason?, result, session, warning? }
997
1041
  ```
998
1042
 
999
- When `inputMode` is `"synthetic"` and the target has hover/enter handlers, the result carries a `warning` so you know a hover may be a no-op — you never have to reverse-engineer it.
1043
+ When `inputMode` is `"synthetic"` and the target has hover/enter handlers, the result carries a `warning` so you know a hover may be a no-op. You never have to reverse-engineer it.
1000
1044
 
1001
- **`inputModeReason` — never a silent fallback.** When real input **is** configured but a pointer act still ran synthetic, the result says _why_, so per-element inconsistency is diagnosable instead of mysterious:
1045
+ **`inputModeReason` is never a silent fallback.** When real input **is** configured but a pointer act still ran synthetic, the result says _why_, so per-element inconsistency is diagnosable instead of mysterious:
1002
1046
 
1003
1047
  | `inputModeReason` | meaning / fix |
1004
1048
  | --- | --- |
1005
- | `page-not-correlated-to-a-cdp-target` | no CDP page matches the session URL — usually a fresh tab or a CDP target that isn't this page |
1006
- | `element-not-locatable` | the element had no box (off-screen / stale ref) — `scrollIntoView` first |
1049
+ | `page-not-correlated-to-a-cdp-target` | no CDP page matches the session URL, usually a fresh tab or a CDP target that isn't this page |
1050
+ | `element-not-locatable` | the element had no box (off-screen / stale ref); `scrollIntoView` first |
1007
1051
  | `drag-target-unresolved` | a drag's `toRef` was missing or not locatable |
1008
1052
  | `provider-declined` / `provider-error` | the CDP provider declined or threw (the latter also sets `warning`) |
1009
- | `not-a-pointer-action` | `fill`/`type`/etc. — these are always synthetic by design |
1010
- | `synthetic-click-preferred` | a `click`/`dblclick` ran the occlusion-honest synthetic path by default — pass `args:{ native:true }` to force a trusted native click |
1053
+ | `not-a-pointer-action` | `fill`/`type`/etc. are always synthetic by design |
1054
+ | `synthetic-click-preferred` | a `click`/`dblclick` ran the occlusion-honest synthetic path by default; pass `args:{ native:true }` to force a trusted native click |
1011
1055
 
1012
- (No `inputModeReason` is set when real input simply isn't configured — synthetic is the expected default there.)
1056
+ (No `inputModeReason` is set when real input simply isn't configured; synthetic is the expected default there.)
1013
1057
 
1014
1058
  ### Enable real input (optional, opt-in)
1015
1059
 
1016
- Point Reticle's server at a Chrome DevTools (CDP) endpoint; it then drives **real** pointer input (via Playwright `connectOverCDP`) at the element's box for `hover`/`drag` (and for `click`/`dblclick` only when you pass `args:{ native:true }` — clicks default to synthetic), and reports `inputMode: "real"`.
1060
+ Point Reticle's server at a Chrome DevTools (CDP) endpoint; it then drives **real** pointer input (via Playwright `connectOverCDP`) at the element's box for `hover`/`drag` (and for `click`/`dblclick` only when you pass `args:{ native:true }`, since clicks default to synthetic), and reports `inputMode: "real"`.
1017
1061
 
1018
1062
  1. Launch your browser with remote debugging:
1019
1063
 
@@ -1037,10 +1081,10 @@ Point Reticle's server at a Chrome DevTools (CDP) endpoint; it then drives **rea
1037
1081
  }
1038
1082
  ```
1039
1083
 
1040
- That's it. Reticle correlates the CDP page to your SDK session by URL; pointer actions now fire native hover/enter so hover-gated suggestion panels, tooltips, and pointer-based drag become drivable. Everything else is unchanged, and with no `RETICLE_CDP_URL` set, Reticle stays in the synthetic (zero-dependency, in-page) mode — Playwright is an optional dependency loaded only when you opt in.
1084
+ That's it. Reticle correlates the CDP page to your SDK session by URL; pointer actions now fire native hover/enter so hover-gated suggestion panels, tooltips, and pointer-based drag become drivable. Everything else is unchanged, and with no `RETICLE_CDP_URL` set, Reticle stays in the synthetic (zero-dependency, in-page) mode. Playwright is an optional dependency loaded only when you opt in.
1041
1085
 
1042
- > **SPA navigation is handled.** The URL correlation tracks client-side route changes (`pushState`/`replaceState`/`popstate`), so real input keeps working after your app navigates into a sub-route — e.g. the hover/quick-edit cluster on a `/workspace` view stays drivable. (If you see `inputModeReason:"page-not-correlated-to-a-cdp-target"`, the reported session URL isn't correlated to a CDP target and real input silently falls back to synthetic.)
1086
+ > **SPA navigation is handled.** The URL correlation tracks client-side route changes (`pushState`/`replaceState`/`popstate`), so real input keeps working after your app navigates into a sub-route, e.g. the hover/quick-edit cluster on a `/workspace` view stays drivable. (If you see `inputModeReason:"page-not-correlated-to-a-cdp-target"`, the reported session URL isn't correlated to a CDP target and real input silently falls back to synthetic.)
1043
1087
 
1044
1088
  > **Watching the agent (presenter).** With `present: true` the activity border now glows once while the agent is busy and fades when idle (no per-action strobe); the HUD sits **bottom-center**, shows a **READING** vs **ACTING** chip so you can tell observation from action at a glance, and `reticle_session {action:"narrate"}` lines are **queued** with a minimum on-screen dwell so none flash by unread.
1045
1089
 
1046
- > **Limitation — un-scriptable tabs.** Reticle observes/drives a tab through the in-page SDK + (optionally) CDP; it **cannot bring to front or recover a browser tab the OS won't let it script** (e.g. a backgrounded or non-default-browser tab reporting `hidden:true`/`throttled:true`). When that happens, `reticle_sessions` and every act/assert result carry a `session.recommendation` saying so and pointing to `reticle drive <url>` for a guaranteed scriptable context — refocus the tab, or use `reticle drive`.
1090
+ > **Limitation: un-scriptable tabs.** Reticle observes/drives a tab through the in-page SDK + (optionally) CDP; it **cannot bring to front or recover a browser tab the OS won't let it script** (e.g. a backgrounded or non-default-browser tab reporting `hidden:true`/`throttled:true`). When that happens, `reticle_sessions` and every act/assert result carry a `session.recommendation` saying so and pointing to `reticle drive <url>` for a guaranteed scriptable context. Refocus the tab, or use `reticle drive`.