@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
@@ -0,0 +1,238 @@
1
+ ---
2
+ title: Instrument your app so it can testify
3
+ sidebarTitle: Instrument
4
+ description: Register stores, emit signals, add testids. The three things that decide whether your verdicts say "the DOM changed" or "the app succeeded".
5
+ icon: wrench
6
+ ---
7
+
8
+ **Three things raise a Reticle verdict from "something appeared" to "the app declared success": register your store, emit named signals, and add testids.** All three live in one dev-only file, `src/reticle-dev.ts`, which `npx @reticlehq/server init` creates for you.
9
+
10
+ Reticle works with zero instrumentation. You get DOM, network, console and routing out of the box, and that is already more than a screenshot can tell you.
11
+
12
+ But there is a ceiling, and this page is about raising it. Instrumentation is the difference between a verdict that says _something appeared_ and one that says _the application declared success_.
13
+
14
+ ## The grade ladder
15
+
16
+ Every verdict carries an `honesty.grade`. How strong the evidence actually was.
17
+
18
+ <CardGroup cols={3}>
19
+ <Card title="presence" icon="eye">
20
+ An element appeared, or text showed up. True, and weak: a mock renders exactly the same.
21
+ </Card>
22
+ <Card title="state" icon="database">
23
+ A registered store changed. Now you know the app accepted it, not just drew it.
24
+ </Card>
25
+ <Card title="signal" icon="tower-broadcast">
26
+ The app fired a named signal. The app itself declaring success. Nothing beats this.
27
+ </Card>
28
+ </CardGroup>
29
+
30
+ Here is the same login, verified two ways. First with a network predicate and no instrumentation leaned on:
31
+
32
+ ```json
33
+ "honesty": { "grade": "presence", "coverage": { "pct": 100 }, "integrity": { "clean": true } }
34
+ ```
35
+
36
+ Then with a signal predicate, against the same app:
37
+
38
+ ```json
39
+ "honesty": { "grade": "signal", "coverage": { "pct": 100 }, "integrity": { "clean": true } }
40
+ ```
41
+
42
+ Same click. Better evidence. The only difference is that somebody spent ten minutes emitting a signal.
43
+
44
+ ## Three things to add
45
+
46
+ Everything lives in `src/reticle-dev.ts`, which `reticle init` creates for you and which is dev-only, it self-guards on `import.meta.env.DEV` and is a no-op in a production build.
47
+
48
+ ### 1. Register your store
49
+
50
+ ```ts
51
+ import { registerStore, registerCapabilities, tanstackQueryStore } from '@reticlehq/browser';
52
+
53
+ registerStore('app', useApp);
54
+ registerStore('queries', tanstackQueryStore(queryClient));
55
+ ```
56
+
57
+ <Warning>
58
+ Pass the **store**, not `() => store.getState()`. The store form wires `subscribe`, so every mutation
59
+ emits a state diff. The getter form is read-only and silently produces empty `stateDiffs`, which
60
+ reads as "nothing changed" and actually means "I was never watching".
61
+ </Warning>
62
+
63
+ That single line is what turns this up in a verdict:
64
+
65
+ ```json
66
+ "stateDiffs": [
67
+ { "path": "auth", "from": null, "to": "{\"email\":\"admin@reticle.dev\"}", "atMs": 833326 }
68
+ ]
69
+ ```
70
+
71
+ A mock that returns `200` without touching state gets caught precisely there, and nowhere else.
72
+
73
+ <Tip>
74
+ Register your server-state cache too. "The screen is plausible and the network is silent" is the
75
+ normal failure with query caches: a mutation forgets to invalidate, the UI keeps rendering a
76
+ number that was true a minute ago, and nothing fires for anyone to notice. The cache's freshness
77
+ metadata is the only witness.
78
+ </Tip>
79
+
80
+ ### 2. Emit signals
81
+
82
+ A signal is your app saying "this specific thing succeeded". Define the names in one place so they cannot drift:
83
+
84
+ ```ts
85
+ import { reticle } from '@reticlehq/browser';
86
+
87
+ export const Sig = {
88
+ AUTH_GRANTED: 'auth:granted',
89
+ AUTH_DENIED: 'auth:denied',
90
+ DEPLOY_CREATED: 'deploy:created',
91
+ MODAL_OPENED: 'modal:opened',
92
+ TOAST_SHOWN: 'toast:shown',
93
+ } as const;
94
+
95
+ export type SigName = (typeof Sig)[keyof typeof Sig];
96
+
97
+ const isDev = import.meta.env.DEV;
98
+
99
+ /** Emit a Reticle signal (dev only, no-op in production). */
100
+ export function emit(name: SigName, data: Record<string, unknown> = {}): void {
101
+ if (isDev) reticle.signal(name, data);
102
+ }
103
+ ```
104
+
105
+ `Sig` is a value, so it cannot be used as a type directly. `SigName` is the union of its values, which is what makes `emit(Sig.AUTH_GRANTED)` check and `emit('auth:granded')` fail at compile time rather than at 2am.
106
+
107
+ Then call `emit(Sig.AUTH_GRANTED, { email })` where the thing actually succeeds. Not where you _think_ it succeeds. In the success branch, after the state update, not in the click handler.
108
+
109
+ **Emit the failures too.** `auth:denied` is as valuable as `auth:granted`, for a reason the next section demonstrates.
110
+
111
+ ### 3. Advertise the surface
112
+
113
+ ```ts
114
+ registerCapabilities({
115
+ testids: TESTIDS,
116
+ signals: Object.values(Sig),
117
+ stores: ['app'],
118
+ flows: FLOWS,
119
+ });
120
+ ```
121
+
122
+ This is what [`reticle_capabilities`](/tools-tools-and-run) returns. The app telling an agent what it considers testable, before the agent has looked at a single element. It is the cheapest and most truthful first call on an unfamiliar codebase.
123
+
124
+ ## Why emitting failure signals pays off
125
+
126
+ Here is a real failed login, from an app that emits both signals:
127
+
128
+ ```json
129
+ {
130
+ "verified": "no",
131
+ "verdict": {
132
+ "pass": false,
133
+ "failureReason": "no signal matched {\"kind\":\"signal\",\"name\":\"auth:granted\"}",
134
+ "observed": "signal 'auth:granted' never fired; signals seen in this window: auth:denied",
135
+ "expected": "signal 'auth:granted' to fire"
136
+ },
137
+ "summary": {
138
+ "net": { "total": 1, "errors": 1, "headline": "POST http://localhost:8787/api/login 401" },
139
+ "stateDiffs": [],
140
+ "signals": ["auth:denied"]
141
+ },
142
+ "honesty": { "grade": "signal", "integrity": { "clean": true } },
143
+ "capsule": {
144
+ "firstDivergence": {
145
+ "expected": { "kind": "signal", "name": "auth:granted" },
146
+ "observed": "signal \"auth:granted\" never fired"
147
+ },
148
+ "blastRadius": ["signal auth:denied"]
149
+ }
150
+ }
151
+ ```
152
+
153
+ Read `observed` again:
154
+
155
+ > signal 'auth:granted' never fired; **signals seen in this window: auth:denied**
156
+
157
+ Without the failure signal, that line would have stopped at "never fired", and the agent would be guessing between a network problem, a broken handler and a wrong password.
158
+
159
+ With it, the app has named its own outcome: the credentials were rejected. The 401 headline confirms it, `stateDiffs: []` proves nothing was written, and the whole diagnosis arrives in one response.
160
+
161
+ <Note>
162
+ `capsule.firstDivergence` and `blastRadius` are saved to disk automatically on a failure, so the
163
+ evidence survives the agent's context window. Look for `capsuleSaved` in the response.
164
+ </Note>
165
+
166
+ ## When Reticle refuses to call it a pass
167
+
168
+ This is the one that surprises people, so here it is in full. A successful login, on a backgrounded tab:
169
+
170
+ ```json
171
+ {
172
+ "verified": "unknown",
173
+ "verifiedReason": "unsettled",
174
+ "because": "the page never settled, so the reaction window may have closed before the app finished",
175
+ "verdict": {
176
+ "pass": true,
177
+ "evidence": { "name": "auth:granted", "data": { "email": "admin@reticle.dev" } }
178
+ },
179
+ "summary": {
180
+ "stateDiffs": [{ "path": "auth", "from": null, "to": "{\"email\":\"admin@reticle.dev\"}" }],
181
+ "signals": ["auth:granted"]
182
+ },
183
+ "honesty": { "grade": "signal", "integrity": { "clean": true } },
184
+ "feedback_invite": "Reticle could not tell what happened here. If you expected otherwise, reticle_feedback. An unknown verdict is our defect, not yours"
185
+ }
186
+ ```
187
+
188
+ The signal fired. State changed. `verdict.pass` is `true`. And `verified` is **`unknown`**, because the page never went quiet inside the observation window, so Reticle cannot promise it saw the whole story.
189
+
190
+ That is the design working as intended. A tool that reports "pass" whenever it happens to catch a matching event, without knowing whether it saw everything, is how you get a confident wrong answer.
191
+
192
+ <Warning>
193
+ The usual cause is a **throttled tab**. A backgrounded browser tab has its timers suppressed,
194
+ which suppresses the quiescence detection Reticle uses to decide the page has settled. Focus the
195
+ tab, or drive a dedicated context. The response tells you: look for `session.throttled` and the
196
+ accompanying `warning`.
197
+ </Warning>
198
+
199
+ Note the last field. When Reticle cannot tell what happened, it invites a bug report and calls the ambiguity its own defect rather than yours. If you hit an `unknown` you believe is wrong, that is worth [sending](/tools-session-and-feedback).
200
+
201
+ ## Testids, briefly
202
+
203
+ ```tsx
204
+ <button data-testid="login-submit">Sign in</button>
205
+ ```
206
+
207
+ Role and text queries survive refactors well. Testids survive redesigns, translations, and the day marketing rewrites every button label. Add them to the elements your important flows actually touch. Not to everything, which is a chore nobody finishes.
208
+
209
+ ## Keeping it honest
210
+
211
+ Signals rot the way every convention rots: someone adds a mutation, forgets the signal, and every verdict for that flow quietly drops from `signal` to `presence`. Nothing goes red. The tests pass. The evidence just gets weaker, invisibly.
212
+
213
+ ```bash
214
+ npm i -D @reticlehq/eslint-plugin
215
+ ```
216
+
217
+ One rule. State changed, so a signal must fire, makes the signal layer self-enforcing. It is the only mechanism we have found that survives contact with a deadline.
218
+
219
+ ## Start with one flow
220
+
221
+ You do not need to describe the whole app, and trying to is the slow path that gets abandoned halfway.
222
+
223
+ 1. Pick your most important flow. The one that would be embarrassing to break.
224
+ 2. Register the store it reads.
225
+ 3. Emit a signal where it succeeds, and one where it fails.
226
+ 4. Add testids to the elements it touches.
227
+ 5. Drive it, and check `honesty.grade` says `signal`.
228
+
229
+ Then stop. Add more when a flow you actually replay needs it.
230
+
231
+ <CardGroup cols={2}>
232
+ <Card title="Prove it" icon="circle-check" href="/tools-act-and-wait">
233
+ Use your new signals as predicates and watch the grade climb.
234
+ </Card>
235
+ <Card title="Lock it in" icon="vial" href="/testing">
236
+ Turn the instrumented flow into a spec that runs on every pull request.
237
+ </Card>
238
+ </CardGroup>
@@ -1,24 +1,30 @@
1
- # Reticle Integration Patterns
1
+ ---
2
+ title: Integration patterns
3
+ description: 'The recommended shape for a real codebase: a minimal production footprint and a signal layer that cannot silently drift.'
4
+ icon: shapes
5
+ ---
6
+
7
+ The recommended shape for a real codebase is three decisions: put `createReticleEmitter()` in one `app/emit.ts` so components depend on an interface and `@reticlehq/browser` never reaches your production bundle; emit signals from the **store layer** rather than from N call sites; and let each feature self-register its testids and signals with `registerReticleDomain`. Then turn on `reticle/require-signal-on-mutation` so the signal layer cannot silently rot.
2
8
 
3
- The basics in [Getting Started](getting-started.md) work with **zero app changes**. This doc is the _recommended_ shape for a real codebase: a minimal production footprint, a signal layer that can't silently drift, and an adoption path that starts paying off on day one — no rewrite required.
9
+ The basics in [Getting Started](getting-started.md) work with **zero app changes**. This doc is the _recommended_ shape for a real codebase: a minimal production footprint, a signal layer that can't silently drift, and an adoption path that starts paying off on day one, with no rewrite required.
4
10
 
5
- - [1 — Start here: reuse what you already have](#1--start-here-reuse-what-you-already-have)
6
- - [2 — Inject the emitter (zero prod bundle)](#2--inject-the-emitter-zero-prod-bundle)
7
- - [3 — Emit signals from the store layer, not N call sites](#3--emit-signals-from-the-store-layer-not-n-call-sites)
8
- - [4 — Self-registering domains (`registerReticleDomain`)](#4--self-registering-domains-registerreticledomain)
9
- - [5 — Keep the signal layer from rotting (`@reticlehq/eslint-plugin`)](#5--keep-the-signal-layer-from-rotting-reticleeslint-plugin)
10
- - [6 — Limitation: un-scriptable tabs → `reticle drive`](#6--limitation-un-scriptable-tabs--reticle-drive)
11
+ - [1. Start here: reuse what you already have](#1-start-here-reuse-what-you-already-have)
12
+ - [2. Inject the emitter (zero prod bundle)](#2-inject-the-emitter-zero-prod-bundle)
13
+ - [3. Emit signals from the store layer, not N call sites](#3-emit-signals-from-the-store-layer-not-n-call-sites)
14
+ - [4. Self-registering domains (`registerReticleDomain`)](#4-self-registering-domains-registerreticledomain)
15
+ - [5. Keep the signal layer from rotting (`@reticlehq/eslint-plugin`)](#5-keep-the-signal-layer-from-rotting-reticleeslint-plugin)
16
+ - [6. Limitation: un-scriptable tabs, use `reticle drive`](#6-limitation-un-scriptable-tabs-use-reticle-drive)
11
17
  - [Checklist](#checklist)
12
18
 
13
19
  ---
14
20
 
15
- ## 1 — Start here: reuse what you already have
21
+ ## 1. Start here: reuse what you already have
16
22
 
17
23
  Adoption is **free → cheap → targeted**. You don't instrument everything; you reuse what you have, then add the handful of facts the DOM can't express.
18
24
 
19
- **1. Reuse your existing `data-testid` (free).** If you already test with Playwright or Cypress, your testids work in Reticle unchanged — `reticle_query({ by: 'testid', value: 'checkout' })` matches them exactly. No new markup, no new code.
25
+ **1. Reuse your existing `data-testid` (free).** If you already test with Playwright or Cypress, your testids work in Reticle unchanged; `reticle_query({ by: 'testid', value: 'checkout' })` matches them exactly. No new markup, no new code.
20
26
 
21
- **2. Advertise the surface from your existing constants (cheap).** You already keep a `TestIds` constant object for your E2E suite — pass it straight in. Now `reticle_capabilities()` tells a fresh agent your whole surface without reading source.
27
+ **2. Advertise the surface from your existing constants (cheap).** You already keep a `TestIds` constant object for your E2E suite. Pass it straight in. Now `reticle_capabilities()` tells a fresh agent your whole surface without reading source.
22
28
 
23
29
  ```ts
24
30
  import { registerCapabilities } from '@reticlehq/browser';
@@ -31,7 +37,7 @@ registerCapabilities({
31
37
  });
32
38
  ```
33
39
 
34
- **3. Add signals only at the ~20 commit points that matter (targeted).** Emit `reticle.signal(name, data)` at the moments the DOM can't show — a save committed, a webhook arrived, an edit applied, an async generation finished. You instrument the off-DOM facts you'd otherwise eyeball, not every line.
40
+ **3. Add signals only at the ~20 commit points that matter (targeted).** Emit `reticle.signal(name, data)` at the moments the DOM can't show: a save committed, a webhook arrived, an edit applied, an async generation finished. You instrument the off-DOM facts you'd otherwise eyeball, not every line.
35
41
 
36
42
  ```ts
37
43
  import { reticle } from '@reticlehq/browser';
@@ -39,36 +45,36 @@ onSaved(() => reticle.signal('order:saved', { id, total }));
39
45
  // agent: reticle_assert({ predicate: { kind: 'signal', name: 'order:saved', dataMatches: { id: '*' } } })
40
46
  ```
41
47
 
42
- That's day-one usefulness with no rewrite. The rest of this doc is how to do steps 2–3 _well_ so the signal layer stays honest as the app grows.
48
+ That's day-one usefulness with no rewrite. The rest of this doc is how to do steps 2 and 3 _well_ so the signal layer stays honest as the app grows.
43
49
 
44
- ## 2 — Inject the emitter (zero prod bundle)
50
+ ## 2. Inject the emitter (zero prod bundle)
45
51
 
46
- The #1 objection: _"I don't want a test tool in my production bundle."_ The answer: components never import `@reticlehq/browser`. They depend on a tiny structural interface, `ReticleEmitter` (`{ signal, state }`), and the real emitter is injected once at the top. `createReticleEmitter()` returns an emitter that proxies to the connected `reticle` singleton and is a **safe no-op** until `reticle.connect()` runs — so nothing breaks in production or before connect, and **`@reticlehq/browser` stays out of the prod bundle.**
52
+ The #1 objection: _"I don't want a test tool in my production bundle."_ The answer: components never import `@reticlehq/browser`. They depend on a tiny structural interface, `ReticleEmitter` (`{ signal, state }`), and the real emitter is injected once at the top. `createReticleEmitter()` returns an emitter that proxies to the connected `reticle` singleton and is a **safe no-op** until `reticle.connect()` runs, so nothing breaks in production or before connect, and **`@reticlehq/browser` stays out of the prod bundle.**
47
53
 
48
54
  ```ts
49
- // app/emit.ts — the one place that touches @reticlehq/browser
55
+ // app/emit.ts: the one place that touches @reticlehq/browser
50
56
  import { createReticleEmitter } from '@reticlehq/browser';
51
57
  export const emit = createReticleEmitter(); // no-op until reticle.connect()
52
58
  ```
53
59
 
54
60
  ```ts
55
- // any component — depends on the interface, not the SDK
61
+ // any component: depends on the interface, not the SDK
56
62
  import { emit } from '../emit';
57
63
  function onSaved(id: string, total: number) {
58
64
  emit.signal('order:saved', { id, total });
59
65
  }
60
66
  ```
61
67
 
62
- The emitter re-checks the connection on every call, so you can create it at module load — before `reticle.connect()` — and it starts forwarding the moment Reticle connects. (See [getting-started Step 2](getting-started.md#step-2--embed-the-sdk-in-your-app) for where `reticle.connect()` goes.) This is the single highest-leverage decision; everything below assumes it.
68
+ The emitter re-checks the connection on every call, so you can create it at module load (before `reticle.connect()`) and it starts forwarding the moment Reticle connects. (See [getting-started Step 2](getting-started.md#step-2-embed-the-sdk-in-your-app) for where `reticle.connect()` goes.) This is the single highest-leverage decision; everything below assumes it.
63
69
 
64
- ## 3 — Emit signals from the store layer, not N call sites
70
+ ## 3. Emit signals from the store layer, not N call sites
65
71
 
66
- The smell: every store mutation hand-emits a signal right after it, and over dozens of call sites the two **drift** — a new mutation path forgets the emit and the contract silently breaks. Drive the signal from where the state actually changes instead.
72
+ The smell: every store mutation hand-emits a signal right after it, and over dozens of call sites the two **drift**: a new mutation path forgets the emit and the contract silently breaks. Drive the signal from where the state actually changes instead.
67
73
 
68
- **Pattern A — store middleware (sketch).** One audited map from action → signal lives next to the store, so _state changed ⇒ signal fired_ is structural, not a thing each call site remembers.
74
+ **Pattern A: store middleware (sketch).** One audited map from action → signal lives next to the store, so _state changed ⇒ signal fired_ is structural, not a thing each call site remembers.
69
75
 
70
76
  ```ts
71
- // store-with-reticle.ts — sketch: one audited transition map, not N call sites
77
+ // store-with-reticle.ts sketch: one audited transition map, not N call sites
72
78
  import { emit } from './emit';
73
79
 
74
80
  const signalFor: Record<string, (next: State) => readonly [string, Record<string, unknown>]> = {
@@ -83,7 +89,7 @@ function dispatch(action: keyof typeof signalFor, run: () => State): void {
83
89
  }
84
90
  ```
85
91
 
86
- **Pattern B — `commitAndSignal` (lighter).** When you don't want a middleware, pair the mutation and its signal in one call that can't drift. It runs `mutate()`, emits the signal exactly once, and returns the mutation's value.
92
+ **Pattern B: `commitAndSignal` (lighter).** When you don't want a middleware, pair the mutation and its signal in one call that can't drift. It runs `mutate()`, emits the signal exactly once, and returns the mutation's value.
87
93
 
88
94
  ```ts
89
95
  import { commitAndSignal } from '@reticlehq/browser';
@@ -97,18 +103,18 @@ const next = commitAndSignal(
97
103
  );
98
104
  ```
99
105
 
100
- If `mutate` throws, the mutation never happened — so **the signal is not emitted and the error propagates** unchanged.
106
+ If `mutate` throws, the mutation never happened, so **the signal is not emitted and the error propagates** unchanged.
101
107
 
102
- > **The documented exception:** genuinely view-level signals — render or async completions like `diff:shown` or `caption:generated`, which aren't store state — legitimately stay in your components. Only commit-point signals belong in the store layer.
108
+ > **The documented exception:** genuinely view-level signals (render or async completions like `diff:shown` or `caption:generated`, which aren't store state) legitimately stay in your components. Only commit-point signals belong in the store layer.
103
109
 
104
110
  Pair this with store registration so the agent can _read_ state instead of you emitting a signal per fact: `registerStore('workspace', useWorkspace)`, then `reticle_state({ store: 'workspace' })`.
105
111
 
106
- ## 4 — Self-registering domains (`registerReticleDomain`)
112
+ ## 4. Self-registering domains (`registerReticleDomain`)
107
113
 
108
- Rather than maintaining one central flat-map of the whole testable surface (and remembering to wire each new area into it), co-locate one `reticle.ts` per domain that exports its `{ testids, signals, stores }` and self-registers. The capability registry assembles itself from every domain — later calls accumulate as a union, with no duplicates.
114
+ Rather than maintaining one central flat-map of the whole testable surface (and remembering to wire each new area into it), co-locate one `reticle.ts` per domain that exports its `{ testids, signals, stores }` and self-registers. The capability registry assembles itself from every domain; later calls accumulate as a union, with no duplicates.
109
115
 
110
116
  ```ts
111
- // features/sections/reticle.ts — co-locate a domain's testids + signals in one module
117
+ // features/sections/reticle.ts: co-locate a domain's testids + signals in one module
112
118
  import { registerReticleDomain } from '@reticlehq/browser';
113
119
 
114
120
  export const SectionTestIds = { list: 'section-list', add: 'section-add' } as const;
@@ -127,11 +133,11 @@ import { registerReticleDomain } from '@reticlehq/browser';
127
133
  registerReticleDomain({ testids: ['search-input'], signals: ['search:ran'] });
128
134
  ```
129
135
 
130
- Importing both modules in dev makes `reticle_capabilities()` return the merged surface (`testids: ['section-list', 'section-add', 'search-input']`, `signals: ['section:reordered', 'search:ran']`, `stores: ['workspace']`). `registerReticleDomain` is a thin convenience over `registerCapabilities` — same merge-idempotent, HMR-safe semantics — so it composes with §1's "use your existing constants." (Named flows stay an explicit `registerCapabilities({ flows })` concern — their last-writer-wins semantics don't fit "accumulate from many domains.")
136
+ Importing both modules in dev makes `reticle_capabilities()` return the merged surface (`testids: ['section-list', 'section-add', 'search-input']`, `signals: ['section:reordered', 'search:ran']`, `stores: ['workspace']`). `registerReticleDomain` is a thin convenience over `registerCapabilities`, with the same merge-idempotent, HMR-safe semantics, so it composes with §1's "use your existing constants." (Named flows stay an explicit `registerCapabilities({ flows })` concern: their last-writer-wins semantics don't fit "accumulate from many domains.")
131
137
 
132
- ## 5 — Keep the signal layer from rotting (`@reticlehq/eslint-plugin`)
138
+ ## 5. Keep the signal layer from rotting (`@reticlehq/eslint-plugin`)
133
139
 
134
- A signal layer silently rots: someone adds a mutation path and forgets the signal, and the agent's contract breaks with no error. The lint rule catches it at the only moment that's cheap — review.
140
+ A signal layer silently rots: someone adds a mutation path and forgets the signal, and the agent's contract breaks with no error. The lint rule catches it at the only moment that's cheap: review.
135
141
 
136
142
  The rule **`reticle/require-signal-on-mutation`** flags a function that calls a configured store mutator but emits no signal in the same function. It is a **safe no-op until you tell it which calls mutate state** (`mutators`) and which call emits a signal (`signalCallee`, default `signal` / `reticleSignal`):
137
143
 
@@ -152,13 +158,13 @@ export default [
152
158
  ];
153
159
  ```
154
160
 
155
- Or turn it on with the shipped preset (warns, with empty no-op defaults you then configure): `plugin.configs.recommended`. A function that calls a mutator and a signal together passes; a mutator with no signal reports `store mutation without a mapped Reticle signal`. The documented view-level exceptions from §3 simply don't list those view callees as `mutators`, so they never fire.
161
+ Or turn it on with the shipped preset: `plugin.configs.recommended`, which sets `reticle/require-signal-on-mutation` to `warn` (with empty no-op defaults you then configure) and the plugin's other rule, `reticle/no-internal-tags`, to `error`. A function that calls a mutator and a signal together passes; a mutator with no signal reports `store mutation without a mapped Reticle signal`. The documented view-level exceptions from §3 simply don't list those view callees as `mutators`, so they never fire.
156
162
 
157
- ## 6 — Limitation: un-scriptable tabs → `reticle drive`
163
+ ## 6. Limitation: un-scriptable tabs, use `reticle drive`
158
164
 
159
- Reticle observes and drives a tab through the in-page SDK plus (optionally) CDP. It **cannot bring to front or recover a browser tab the OS won't let it script** — e.g. a backgrounded tab, or a non-default browser (Dia, etc.) reporting `hidden:true` / `throttled:true`.
165
+ Reticle observes and drives a tab through the in-page SDK plus (optionally) CDP. It **cannot bring to front or recover a browser tab the OS won't let it script**: a backgrounded tab, say, or a non-default browser (Dia, etc.) reporting `hidden:true` / `throttled:true`.
160
166
 
161
- When that happens, `reticle_sessions` and every act/assert result carry a `session.recommendation` saying so. The escape hatch is **`reticle drive <url>`** (add `--headed` to watch) — Reticle launches and owns a guaranteed-scriptable browser. See [usage §18](usage.md#18-real-input-mode--native-hover--drag) for the full note.
167
+ When that happens, `reticle_sessions` and every act/assert result carry a `session.recommendation` saying so. The escape hatch is **`reticle drive <url>`** (add `--headed` to watch). Reticle launches and owns a guaranteed-scriptable browser. See [usage §18](usage.md#18-real-input-mode-native-hover--drag) for the full note.
162
168
 
163
169
  ## Checklist
164
170
 
@@ -1,8 +1,14 @@
1
- # Test unpublished Reticle changes in a real app (local registry)
1
+ ---
2
+ title: Local registry
3
+ description: 'Exercise local, unpublished Reticle changes in a real external app before they ship.'
4
+ icon: box
5
+ ---
2
6
 
3
- > **For normal use, Reticle is on public npm** — just `npm i -D @reticlehq/react @reticlehq/vite-plugin` (see [Getting Started](getting-started.md)). You only need this guide to test **local, unpublished changes** to the Reticle packages in a real external app before they ship.
7
+ To test unpublished `@reticlehq/*` changes in a real external app, publish the workspace to a local Verdaccio with `bash scripts/local-registry.sh`, then point that app's `.npmrc` at `http://localhost:4873/` for the `@reticlehq` scope. That is the whole procedure; the rest of this page is the detail.
4
8
 
5
- Because the `@reticlehq/*` packages depend on each other via the workspace protocol, plain `npm pack` tarballs don't resolve cleanly. The reliable way to exercise your in-progress changes in a real app is a tiny **local registry** (Verdaccio) — the same path CI uses to validate a publish.
9
+ > **For normal use, Reticle is on public npm.** Just `npm i -D @reticlehq/react @reticlehq/vite-plugin` (see [Getting Started](getting-started.md)). You only need this guide to test **local, unpublished changes** to the Reticle packages in a real external app before they ship.
10
+
11
+ Because the `@reticlehq/*` packages depend on each other via the workspace protocol, plain `npm pack` tarballs don't resolve cleanly. The reliable way to exercise your in-progress changes in a real app is a tiny **local registry** (Verdaccio), the same path CI uses to validate a publish.
6
12
 
7
13
  ## 1. Publish @reticlehq/\* to a local registry
8
14
 
@@ -16,7 +22,7 @@ This starts a **fresh** Verdaccio on `http://localhost:4873`, creates a user/tok
16
22
 
17
23
  | Package | What you install it for |
18
24
  | --- | --- |
19
- | **`@reticlehq/react`** | **install this** — the browser SDK kit you embed (re-exports the browser sensor, so one install gives both `reticle` and `install`) |
25
+ | **`@reticlehq/react`** | **install this**: the browser SDK kit you embed (re-exports the browser sensor, so one install gives both `reticle` and `install`) |
20
26
  | `@reticlehq/vite-plugin` | dev-only source mapping + `connect()` injection (Vite) |
21
27
  | `@reticlehq/next` | Next.js build wrapper (`withReticle`) |
22
28
  | `@reticlehq/server` | the bridge + MCP server (your agent runs it, `npx @reticlehq/server mcp`) |
@@ -31,7 +37,7 @@ For a browser app, install `@reticlehq/react` plus the build plugin for your fra
31
37
 
32
38
  ## 2. Point your app at the local registry
33
39
 
34
- In your app's project root, add an `.npmrc` (scopes only `@reticle` to the local registry; everything else still comes from npm):
40
+ In your app's project root, add an `.npmrc`. The scope is **`@reticlehq`**, not `@reticle`: a scope typo here does not error, it silently resolves the PUBLISHED packages from npm, so you measure shipped code and think you tested your branch. Everything outside that scope still comes from npm:
35
41
 
36
42
  ```ini
37
43
  @reticlehq:registry=http://localhost:4873/
@@ -47,18 +53,18 @@ npm i -D @reticlehq/react @reticlehq/vite-plugin
47
53
  # optional: npm i -D @reticlehq/eslint-plugin # require-signal-on-mutation lint rule
48
54
  ```
49
55
 
50
- Then follow [Getting Started](getting-started.md): embed `reticle.connect()` (dev only) from `@reticlehq/react`, add the MCP server to your agent, and (React) `install()` the adapter from `@reticlehq/react`. For the fastest agent loop, also do [Step 6 — make your app agent-legible](getting-started.md) (testids, `reticle.signal`, `registerStore`, `registerCapabilities`) and the [integration patterns](integration-patterns.md) (`createReticleEmitter` for zero prod-bundle cost).
56
+ Then follow [Getting Started](getting-started.md): embed `reticle.connect()` (dev only) from `@reticlehq/react`, add the MCP server to your agent, and (React) `install()` the adapter from `@reticlehq/react`. For the fastest agent loop, also do [Step 6: make your app agent-legible](getting-started.md) (testids, `reticle.signal`, `registerStore`, `registerCapabilities`) and the [integration patterns](integration-patterns.md) (`createReticleEmitter` for zero prod-bundle cost).
51
57
 
52
- > **Upgrading.** The packages are currently **1.2.0**; new tools land as minor bumps. `scripts/local-registry.sh` resets Verdaccio and republishes the current version, so pull the latest in your app explicitly — `npm install @reticlehq/react@latest`:
58
+ > **Upgrading.** `scripts/local-registry.sh` resets Verdaccio and republishes at whatever version the workspace is on right now, so your app will not pick up a rebuild on its own. Pull it explicitly with `@latest`:
53
59
  >
54
60
  > ```bash
55
61
  > npm i -D @reticlehq/react@latest @reticlehq/vite-plugin@latest @reticlehq/eslint-plugin@latest
56
62
  > ```
57
63
 
58
- **Run the MCP server** from the local registry too — `npx @reticlehq/server` _is_ the server:
64
+ **Run the MCP server** from the local registry too; `npx @reticlehq/server` _is_ the server:
59
65
 
60
66
  ```jsonc
61
- // .mcp.json — point npx at the local registry so it fetches @reticlehq/server from Verdaccio
67
+ // .mcp.json: point npx at the local registry so it fetches @reticlehq/server from Verdaccio
62
68
  {
63
69
  "mcpServers": {
64
70
  "reticle": {
@@ -86,7 +92,7 @@ Mount the SDK from a dev-only client component (see the Next.js section in [Gett
86
92
 
87
93
  Synthetic events can't trigger native `onMouseEnter`/pointer state (hover menus, tooltips, pointer drag). Enable **real input** so the server drives genuine pointer input and `reticle_act` reports `inputMode:"real"`:
88
94
 
89
- - **Easiest — `reticle drive`:** Reticle launches its own scriptable, headless-capable browser at your app URL (no flags to juggle):
95
+ - **Easiest (`reticle drive`):** Reticle launches its own scriptable, headless-capable browser at your app URL (no flags to juggle):
90
96
 
91
97
  ```bash
92
98
  npx --registry http://localhost:4873/ @reticlehq/server drive http://localhost:4310 # add --headed to watch
@@ -107,16 +113,16 @@ Synthetic events can't trigger native `onMouseEnter`/pointer state (hover menus,
107
113
  }
108
114
  ```
109
115
 
110
- With neither set, Reticle stays synthetic (zero extra deps) and says so via `inputMode`. See [usage §18](usage.md#18-real-input-mode--native-hover--drag).
116
+ With neither set, Reticle stays synthetic (zero extra deps) and says so via `inputMode`. See [usage §18](usage.md#18-real-input-mode-native-hover--drag).
111
117
 
112
118
  ## Write replayable specs + git-checked flows
113
119
 
114
- - **Specs:** with `@reticlehq/test`, turn checks into `reticleTest("…", async t => { await t.act(...); await t.expectSignal(...) })` — signal/testid-bound, `reticle_clock` for determinism, `t.expectInputModeReal()` to skip-with-reason when real input isn't active. Run them headless via `reticle drive` (the same path CI uses).
120
+ - **Specs:** with `@reticlehq/test`, turn checks into `reticleTest("…", async t => { await t.act(...); await t.expectSignal(...) })`. They are signal/testid-bound, `reticle_clock` for determinism, `t.expectInputModeReal()` to skip-with-reason when real input isn't active. Run them headless via `reticle drive` (the same path CI uses).
115
121
  - **Flows:** record a flow once and Reticle writes it to a git-checked `.reticle/flows/<name>.json` (anchored on testid/signal); `reticle_flow_replay` re-resolves anchors at run time and reports **legible drift** with a nearest-match; `reticle_flow_heal` proposes/applies the rebind. A fresh agent reads `.reticle/contract.json` to learn your testable surface without grepping source.
116
122
 
117
123
  ## When you're ready for real npm
118
124
 
119
- The same packages publish to public npm unchanged — `pnpm -r publish --access public` after `npm login`. The Verdaccio run above is a faithful rehearsal of that.
125
+ The same packages publish to public npm unchanged: `pnpm -r publish --access public` after `npm login`. The Verdaccio run above is a faithful rehearsal of that.
120
126
 
121
127
  ## Cleanup
122
128
 
@@ -1,22 +1,28 @@
1
- # Multi-agent & multi-project testing
1
+ ---
2
+ title: 'Multi-agent and multi-project'
3
+ description: 'Several apps at once, ports that shift between runs, and many agents driving the same app in parallel without a Chromium each.'
4
+ icon: users
5
+ ---
2
6
 
3
- Reticle is built for the messy real world: several apps running at once, ports that shift between runs, and many agents driving different flows of the same app in parallel — without each one spinning up its own Chromium. This page explains how that works and how to use it.
7
+ Many agents can drive the same app at once without a browser each: `reticle_lease {action:"acquire"}` hands each one an **isolated context** (its own cookies, storage and DOM) from a single shared headless Chromium. One daemon per machine, and identity is the **app** (a stable `projectId` stamped by the build plugin), not the port, so an app that boots on `:3001` today is still the same app.
8
+
9
+ Reticle is built for the messy real world: several apps running at once, ports that shift between runs, and many agents driving different flows of the same app in parallel, without each one spinning up its own Chromium. This page explains how that works and how to use it.
4
10
 
5
11
  ## The mental model
6
12
 
7
13
  - **One daemon per machine.** `reticle mcp` discovers a running daemon (via `~/.reticle`) or starts one. A crashed daemon's stale pidfile is reclaimed automatically, so you never chase "port already in use" or a zombie server.
8
- - **Identity is the app, not the port.** The build plugin stamps a stable `projectId` that travels in every connection. If your Next app usually runs on `:3000` but boots on `:3001` today, Reticle still knows which app it is — and an agent scoped to project A will never accidentally drive project B's tab. Origin is only a fallback hint.
9
- - **One browser, many contexts.** When agents need their own headless tabs, the daemon's **browser pool** launches a single Chromium and hands out isolated contexts (one per flow) — cheap, and capped so a big fan-out can't exhaust the machine. Over-cap requests queue.
10
- - **Attach-only.** Reticle never starts your dev server. It connects to an app you're already running (or opens a headless tab pointed at it).
14
+ - **Identity is the app, not the port.** The build plugin stamps a stable `projectId` that travels in every connection. If your Next app usually runs on `:3000` but boots on `:3001` today, Reticle still knows which app it is, and an agent scoped to project A will never accidentally drive project B's tab. Origin is only a fallback hint.
15
+ - **One browser, many contexts.** When agents need their own headless tabs, the daemon's **browser pool** launches a single Chromium and hands out isolated contexts (one per flow). They are cheap, and capped so a big fan-out can't exhaust the machine. Over-cap requests queue.
16
+ - **Attach-only daemon.** The daemon never starts your dev server. It connects to an app that is running (or opens a headless tab pointed at it). When nothing is running, the agent starts your project's own dev script in the background and tells you. A build process spawned by a background daemon would be invisible, unconsented, and orphaned when the daemon exits.
11
17
 
12
- ## Manual testing — ~5 minutes
18
+ ## Manual testing: ~5 minutes
13
19
 
14
20
  1. Add the plugin (Vite/Next) or one `reticle.connect()` call. (See [getting-started](./getting-started.md).)
15
21
  2. Start your app as you normally do.
16
- 3. Open it in a browser — the in-page panel shows Reticle is connected.
22
+ 3. Open it in a browser; the in-page panel shows Reticle is connected.
17
23
  4. Click around; flag anything that looks wrong with the "Flag a bug" annotator. The agent drains those with `reticle_session {action:"review"}`.
18
24
 
19
- ## Agent testing — ~2 minutes
25
+ ## Agent testing: ~2 minutes
20
26
 
21
27
  With the app running and instrumented, an agent drives a flow end to end:
22
28
 
@@ -28,7 +34,7 @@ reticle_assert { sessionId, ... } # verify intent
28
34
  reticle_lease {action:"release"} { sessionId } # free the slot
29
35
  ```
30
36
 
31
- `reticle_lease {action:"acquire"}` opens a fresh isolated headless context against your **already-running** app, stamps the lease identity into the URL so the app's own SDK registers under a sessionId you can target, and waits until that tab has connected (`ready: true`) before returning — so the sessionId is usable immediately. Release when the flow finishes.
37
+ `reticle_lease {action:"acquire"}` opens a fresh isolated headless context against your **already-running** app, stamps the lease identity into the URL so the app's own SDK registers under a sessionId you can target, and waits until that tab has connected (`ready: true`) before returning, so the sessionId is usable immediately. Release when the flow finishes.
32
38
 
33
39
  ## 10 agents, 10 flows, one dashboard
34
40
 
@@ -36,7 +42,7 @@ This is the design target, and it needs no special setup:
36
42
 
37
43
  - Each agent calls `reticle_lease {action:"acquire"}` for the same dashboard URL → its own isolated context (own cookies/storage) in the **one** shared Chromium.
38
44
  - The pool caps simultaneous contexts (`RETICLE_MAX_CONTEXTS`, default scales with CPU under a ceiling); extra acquires queue and proceed as slots free.
39
- - Flows can't bleed into each other — contexts are isolated and every session is scoped by `projectId`.
45
+ - Flows can't bleed into each other: contexts are isolated and every session is scoped by `projectId`.
40
46
  - A single crashed page is reclaimed on its own, and if an agent crashes or hangs its lease stops being touched and the **lease reaper** reclaims the context after a TTL, freeing the slot. One dead agent never starves the others.
41
47
 
42
48
  `reticle_sessions` lists everything with `projectId` (group by app) and `leased` (pool context vs a human tab), so an orchestrator can see the whole fleet at a glance.
@@ -46,10 +52,10 @@ This is the design target, and it needs no special setup:
46
52
  | Env | Default | Effect |
47
53
  | --- | --- | --- |
48
54
  | `RETICLE_MAX_CONTEXTS` | `min(8, cpus-1)` | Max simultaneous leased headless contexts. |
49
- | `RETICLE_PORT` | from `.reticle.json`, else `4400` | Daemon port (rarely needed — discovery handles it). |
55
+ | `RETICLE_PORT` | from `.reticle.json`, else `4400` | Daemon port (rarely needed; discovery handles it). |
50
56
 
51
57
  ## Why not just open many browsers?
52
58
 
53
- Ten Chromiums is hundreds of MB each and will thrash a laptop. Ten contexts in one browser is a few MB apiece and fully isolated — same correctness, a fraction of the cost. That's the whole point of the pool.
59
+ Ten Chromiums is hundreds of MB each and will thrash a laptop. Ten contexts in one browser is a few MB apiece and fully isolated: same correctness, a fraction of the cost. That's the whole point of the pool.
54
60
 
55
- **Measured** (`bench/harness/multi-agent-throughput.mjs`): 16 verification flows that take **35.4s** one-at-a-time finish in **5.2s** across 8 leased contexts on a single Chromium — **6.78× faster**, ~30s saved per batch, with all 8 contexts live at peak. The speed-up scales with agent count up to the cap; the win over launching a browser per agent grows with how much per-agent browser startup you avoid.
61
+ **Measured** (`bench/harness/multi-agent-throughput.mjs`): 16 verification flows that take **35.4s** one-at-a-time finish in **5.2s** across 8 leased contexts on a single Chromium (**6.78× faster**, ~30s saved per batch), with all 8 contexts live at peak. The speed-up scales with agent count up to the cap; the win over launching a browser per agent grows with how much per-agent browser startup you avoid.