@reticlehq/server 2.7.0 → 2.8.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 (236) hide show
  1. package/README.md +14 -5
  2. package/SKILL.md +64 -29
  3. package/dist/capsule/causal-summary.d.ts +20 -0
  4. package/dist/capsule/causal-summary.js +31 -6
  5. package/dist/capsule/divergence.js +7 -1
  6. package/dist/cli/chromium-hint.d.ts +33 -5
  7. package/dist/cli/chromium-hint.js +82 -12
  8. package/dist/cli/cli-doctor.js +28 -25
  9. package/dist/cli/cli-kill.d.ts +58 -0
  10. package/dist/cli/cli-kill.js +162 -0
  11. package/dist/cli/cli-launch.d.ts +17 -0
  12. package/dist/cli/cli-launch.js +28 -0
  13. package/dist/cli/cli-parse.d.ts +16 -1
  14. package/dist/cli/cli-parse.js +32 -9
  15. package/dist/cli/cli-port.d.ts +8 -0
  16. package/dist/cli/cli-port.js +16 -0
  17. package/dist/cli/cli-verify.js +2 -2
  18. package/dist/cli/doctor-sessions-line.d.ts +12 -0
  19. package/dist/cli/doctor-sessions-line.js +66 -0
  20. package/dist/cli/port-holder.d.ts +10 -0
  21. package/dist/cli/port-holder.js +26 -2
  22. package/dist/cli.js +99 -14
  23. package/dist/crawl/crawl-tools.js +12 -2
  24. package/dist/crawl/crawl.d.ts +13 -0
  25. package/dist/crawl/crawl.js +50 -2
  26. package/dist/events/contradictions.d.ts +14 -0
  27. package/dist/events/contradictions.js +26 -17
  28. package/dist/events/predicate-eval.d.ts +20 -1
  29. package/dist/events/predicate-eval.js +125 -3
  30. package/dist/events/predicate.js +170 -18
  31. package/dist/flows/flow-classify.js +1 -1
  32. package/dist/flows/flow-tools.js +12 -4
  33. package/dist/flows/heal-run.js +1 -1
  34. package/dist/honesty/blind-spots.js +6 -2
  35. package/dist/honesty/unsettled.d.ts +40 -0
  36. package/dist/honesty/unsettled.js +78 -0
  37. package/dist/honesty/verified.d.ts +7 -0
  38. package/dist/honesty/verified.js +3 -3
  39. package/dist/index.js +3 -0
  40. package/dist/init/agent-rules.d.ts +16 -0
  41. package/dist/init/agent-rules.js +96 -22
  42. package/dist/init/astro-patch.d.ts +2 -1
  43. package/dist/init/astro-patch.js +11 -5
  44. package/dist/init/capabilities.d.ts +22 -0
  45. package/dist/init/capabilities.js +76 -0
  46. package/dist/init/confirm.d.ts +96 -0
  47. package/dist/init/confirm.js +138 -0
  48. package/dist/init/cra.js +3 -2
  49. package/dist/init/non-js-project.js +29 -5
  50. package/dist/init/plan-framework.js +53 -15
  51. package/dist/init/plan.d.ts +40 -4
  52. package/dist/init/plan.js +163 -40
  53. package/dist/init/run.d.ts +24 -2
  54. package/dist/init/run.js +148 -42
  55. package/dist/init/slash-command.d.ts +1 -1
  56. package/dist/init/slash-command.js +46 -6
  57. package/dist/init/snippets.d.ts +44 -4
  58. package/dist/init/snippets.js +161 -37
  59. package/dist/init/vite-config.js +29 -3
  60. package/dist/input/network-mock-tools.d.ts +19 -0
  61. package/dist/input/network-mock-tools.js +29 -4
  62. package/dist/license/license.js +1 -1
  63. package/dist/mcp/mcp-proxy.d.ts +69 -3
  64. package/dist/mcp/mcp-proxy.js +205 -20
  65. package/dist/mcp/mcp.js +23 -7
  66. package/dist/mcp/proxy-lifecycle.d.ts +1 -1
  67. package/dist/mcp/proxy-lifecycle.js +1 -1
  68. package/dist/pool/browser-pool.d.ts +22 -2
  69. package/dist/pool/browser-pool.js +44 -4
  70. package/dist/project/learned-routes.d.ts +24 -0
  71. package/dist/project/learned-routes.js +63 -0
  72. package/dist/project/project-store.d.ts +6 -0
  73. package/dist/project/project-store.js +25 -1
  74. package/dist/session/no-session-diagnosis.d.ts +19 -0
  75. package/dist/session/no-session-diagnosis.js +71 -18
  76. package/dist/session/no-session-watch.js +12 -0
  77. package/dist/session/ref-provenance.d.ts +54 -0
  78. package/dist/session/ref-provenance.js +61 -0
  79. package/dist/session/session-lease.js +1 -1
  80. package/dist/telemetry/automation-hint.d.ts +17 -0
  81. package/dist/telemetry/automation-hint.js +71 -0
  82. package/dist/telemetry/bug-found.d.ts +0 -20
  83. package/dist/telemetry/bug-found.js +38 -21
  84. package/dist/telemetry/cli-telemetry.js +7 -1
  85. package/dist/telemetry/feedback.js +1 -1
  86. package/dist/telemetry/init-telemetry.js +9 -1
  87. package/dist/telemetry/install-source.d.ts +36 -0
  88. package/dist/telemetry/install-source.js +40 -0
  89. package/dist/telemetry/run-telemetry.js +4 -1
  90. package/dist/telemetry/session-metrics.js +5 -0
  91. package/dist/telemetry/telemetry.d.ts +5 -1
  92. package/dist/telemetry/telemetry.js +15 -2
  93. package/dist/telemetry/tool-refused.d.ts +19 -0
  94. package/dist/telemetry/tool-refused.js +55 -0
  95. package/dist/telemetry/verification-of.js +46 -7
  96. package/dist/temp-dir.d.ts +27 -0
  97. package/dist/temp-dir.js +46 -0
  98. package/dist/tools/act-tools.js +45 -1
  99. package/dist/tools/assert-grade.d.ts +1 -1
  100. package/dist/tools/assert-grade.js +27 -5
  101. package/dist/tools/assert-verdict.js +18 -1
  102. package/dist/tools/contract-tools.js +4 -0
  103. package/dist/tools/dynamic-tools.js +1 -1
  104. package/dist/tools/error-recovery.d.ts +10 -0
  105. package/dist/tools/error-recovery.js +64 -4
  106. package/dist/tools/feedback-invite.js +1 -1
  107. package/dist/tools/invoke-tool.js +139 -23
  108. package/dist/tools/lease-tools.d.ts +29 -1
  109. package/dist/tools/lease-tools.js +58 -5
  110. package/dist/tools/navigate-result.js +1 -1
  111. package/dist/tools/observe-tools.js +1 -1
  112. package/dist/tools/observed-nothing.js +1 -1
  113. package/dist/tools/react-grace.d.ts +48 -0
  114. package/dist/tools/react-grace.js +87 -0
  115. package/dist/tools/read-tools.js +20 -4
  116. package/dist/tools/settle-in-flight.d.ts +79 -0
  117. package/dist/tools/settle-in-flight.js +106 -0
  118. package/dist/tools/snapshot-delta.d.ts +7 -0
  119. package/dist/tools/snapshot-delta.js +130 -6
  120. package/dist/tools/tool-surface.js +19 -0
  121. package/dist/tools/tools.js +16 -4
  122. package/docs/README.md +56 -11
  123. package/docs/actions.mdx +87 -0
  124. package/docs/agent-cheatsheet.md +93 -29
  125. package/docs/architecture.md +46 -35
  126. package/docs/benchmarks.md +76 -72
  127. package/docs/best-practices.mdx +211 -0
  128. package/docs/capabilities.mdx +387 -0
  129. package/docs/cli/affected.mdx +51 -0
  130. package/docs/cli/capsules.mdx +69 -0
  131. package/docs/cli/cloud.mdx +127 -0
  132. package/docs/cli/doctor.mdx +76 -0
  133. package/docs/cli/drive.mdx +55 -0
  134. package/docs/cli/feedback.mdx +67 -0
  135. package/docs/cli/gate.mdx +64 -0
  136. package/docs/cli/help.mdx +92 -0
  137. package/docs/cli/hunt.mdx +57 -0
  138. package/docs/cli/identify.mdx +62 -0
  139. package/docs/cli/init.mdx +94 -0
  140. package/docs/cli/kill.mdx +91 -0
  141. package/docs/cli/license.mdx +48 -0
  142. package/docs/cli/mcp.mdx +61 -0
  143. package/docs/cli/open.mdx +68 -0
  144. package/docs/cli/restart.mdx +60 -0
  145. package/docs/cli/rollback.mdx +35 -0
  146. package/docs/cli/serve.mdx +62 -0
  147. package/docs/cli/status.mdx +64 -0
  148. package/docs/cli/stop.mdx +56 -0
  149. package/docs/cli/telemetry.mdx +57 -0
  150. package/docs/cli/update.mdx +52 -0
  151. package/docs/cli/verify.mdx +57 -0
  152. package/docs/cli/version.mdx +39 -0
  153. package/docs/cli/watch.mdx +55 -0
  154. package/docs/cli.mdx +219 -0
  155. package/docs/debugging.md +13 -9
  156. package/docs/deploy-checks.md +15 -11
  157. package/docs/desktop-apps.md +49 -40
  158. package/docs/desktop.mdx +188 -0
  159. package/docs/docs.json +363 -0
  160. package/docs/enterprise.md +20 -16
  161. package/docs/faq.mdx +88 -0
  162. package/docs/fixtures.md +11 -7
  163. package/docs/flows.md +26 -22
  164. package/docs/for-agents.md +71 -0
  165. package/docs/frameworks.mdx +199 -0
  166. package/docs/gate-plan.md +45 -41
  167. package/docs/gates.md +22 -18
  168. package/docs/getting-started.md +76 -65
  169. package/docs/human-control.md +22 -18
  170. package/docs/index.mdx +101 -0
  171. package/docs/install-agentic.mdx +215 -0
  172. package/docs/install-manual.mdx +194 -0
  173. package/docs/instrumentation.mdx +232 -0
  174. package/docs/integration-patterns.md +38 -34
  175. package/docs/local-registry.md +16 -12
  176. package/docs/multi-agent-testing.md +16 -12
  177. package/docs/packages/babel-plugin.mdx +70 -0
  178. package/docs/packages/browser.mdx +111 -0
  179. package/docs/packages/core.mdx +106 -0
  180. package/docs/packages/electron.mdx +87 -0
  181. package/docs/packages/eslint-plugin.mdx +84 -0
  182. package/docs/packages/next.mdx +91 -0
  183. package/docs/packages/react.mdx +91 -0
  184. package/docs/packages/server.mdx +98 -0
  185. package/docs/packages/tauri.mdx +63 -0
  186. package/docs/packages/test.mdx +108 -0
  187. package/docs/packages/vite-plugin.mdx +96 -0
  188. package/docs/packages.mdx +217 -0
  189. package/docs/platform-integration.md +33 -26
  190. package/docs/predicates.mdx +179 -0
  191. package/docs/quickstart.mdx +211 -0
  192. package/docs/recipes.mdx +242 -0
  193. package/docs/skill-file.mdx +105 -0
  194. package/docs/state-management.mdx +197 -0
  195. package/docs/style.css +14 -0
  196. package/docs/system-map.md +47 -32
  197. package/docs/telemetry-contract.md +93 -50
  198. package/docs/telemetry-events.mdx +430 -0
  199. package/docs/telemetry.md +57 -52
  200. package/docs/testing.md +10 -6
  201. package/docs/token-efficiency.md +30 -26
  202. package/docs/tools-act-and-wait.mdx +123 -0
  203. package/docs/tools-act-sequence.mdx +120 -0
  204. package/docs/tools-act.mdx +82 -0
  205. package/docs/tools-assert.mdx +70 -0
  206. package/docs/tools-console.mdx +59 -0
  207. package/docs/tools-inspect.mdx +85 -0
  208. package/docs/tools-navigate.mdx +63 -0
  209. package/docs/tools-network.mdx +86 -0
  210. package/docs/tools-observe.mdx +89 -0
  211. package/docs/tools-overview.mdx +157 -0
  212. package/docs/tools-query.mdx +82 -0
  213. package/docs/tools-session-and-feedback.mdx +91 -0
  214. package/docs/tools-sessions.mdx +65 -0
  215. package/docs/tools-snapshot.mdx +75 -0
  216. package/docs/tools-state.mdx +81 -0
  217. package/docs/tools-tools-and-run.mdx +125 -0
  218. package/docs/tools-wait-for.mdx +66 -0
  219. package/docs/troubleshooting.mdx +193 -0
  220. package/docs/usage.md +175 -153
  221. package/docs/vs-chrome-devtools-mcp.mdx +82 -0
  222. package/docs/vs-playwright-mcp.mdx +85 -0
  223. package/docs/vs-screenshots.mdx +96 -0
  224. package/docs/what-is-reticle.mdx +63 -0
  225. package/docs/why-reticle.mdx +107 -0
  226. package/package.json +2 -2
  227. package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
  228. package/docs/matrix/2.5.0/codex-machine.json +0 -20
  229. package/docs/matrix/2.5.0/cursor-machine.json +0 -22
  230. package/docs/matrix/2.5.0/gemini-machine.json +0 -22
  231. package/docs/matrix/2.5.0/opencode-machine.json +0 -22
  232. package/docs/matrix/2.5.0/vscode-machine.json +0 -22
  233. package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
  234. package/docs/matrix/MATRIX.md +0 -26
  235. package/docs/matrix/README.md +0 -59
  236. package/docs/mint.json +0 -61
@@ -0,0 +1,232 @@
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
+ 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.
9
+
10
+ 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_.
11
+
12
+ ## The grade ladder
13
+
14
+ Every verdict carries an `honesty.grade`. How strong the evidence actually was.
15
+
16
+ <CardGroup cols={3}>
17
+ <Card title="presence" icon="eye">
18
+ An element appeared, or text showed up. True, and weak: a mock renders exactly the same.
19
+ </Card>
20
+ <Card title="state" icon="database">
21
+ A registered store changed. Now you know the app accepted it, not just drew it.
22
+ </Card>
23
+ <Card title="signal" icon="tower-broadcast">
24
+ The app fired a named signal. The app itself declaring success. Nothing beats this.
25
+ </Card>
26
+ </CardGroup>
27
+
28
+ Here is the same login, verified two ways. First with a network predicate and no instrumentation leaned on:
29
+
30
+ ```json
31
+ "honesty": { "grade": "presence", "coverage": { "pct": 100 }, "integrity": { "clean": true } }
32
+ ```
33
+
34
+ Then with a signal predicate, against the same app:
35
+
36
+ ```json
37
+ "honesty": { "grade": "signal", "coverage": { "pct": 100 }, "integrity": { "clean": true } }
38
+ ```
39
+
40
+ Same click. Better evidence. The only difference is that somebody spent ten minutes emitting a signal.
41
+
42
+ ## Three things to add
43
+
44
+ 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.
45
+
46
+ ### 1. Register your store
47
+
48
+ ```ts
49
+ import { registerStore, registerCapabilities, tanstackQueryStore } from '@reticlehq/browser';
50
+
51
+ registerStore('app', useApp);
52
+ registerStore('queries', tanstackQueryStore(queryClient));
53
+ ```
54
+
55
+ <Warning>
56
+ Pass the **store**, not `() => store.getState()`. The store form wires `subscribe`, so every mutation
57
+ emits a state diff. The getter form is read-only and silently produces empty `stateDiffs`, which
58
+ reads as "nothing changed" and actually means "I was never watching".
59
+ </Warning>
60
+
61
+ That single line is what turns this up in a verdict:
62
+
63
+ ```json
64
+ "stateDiffs": [
65
+ { "path": "auth", "from": null, "to": "{\"email\":\"admin@reticle.dev\"}", "atMs": 833326 }
66
+ ]
67
+ ```
68
+
69
+ A mock that returns `200` without touching state gets caught precisely there, and nowhere else.
70
+
71
+ <Tip>
72
+ Register your server-state cache too. "The screen is plausible and the network is silent" is the
73
+ normal failure with query caches: a mutation forgets to invalidate, the UI keeps rendering a
74
+ number that was true a minute ago, and nothing fires for anyone to notice. The cache's freshness
75
+ metadata is the only witness.
76
+ </Tip>
77
+
78
+ ### 2. Emit signals
79
+
80
+ A signal is your app saying "this specific thing succeeded". Define the names in one place so they cannot drift:
81
+
82
+ ```ts
83
+ import { reticle } from '@reticlehq/browser';
84
+
85
+ export const Sig = {
86
+ AUTH_GRANTED: 'auth:granted',
87
+ AUTH_DENIED: 'auth:denied',
88
+ DEPLOY_CREATED: 'deploy:created',
89
+ MODAL_OPENED: 'modal:opened',
90
+ TOAST_SHOWN: 'toast:shown',
91
+ } as const;
92
+
93
+ const isDev = import.meta.env.DEV;
94
+
95
+ /** Emit a Reticle signal (dev only, no-op in production). */
96
+ export function emit(name: Sig, data: Record<string, unknown> = {}): void {
97
+ if (isDev) reticle.signal(name, data);
98
+ }
99
+ ```
100
+
101
+ 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.
102
+
103
+ **Emit the failures too.** `auth:denied` is as valuable as `auth:granted`, for a reason the next section demonstrates.
104
+
105
+ ### 3. Advertise the surface
106
+
107
+ ```ts
108
+ registerCapabilities({
109
+ testids: TESTIDS,
110
+ signals: Object.values(Sig),
111
+ stores: ['app'],
112
+ flows: FLOWS,
113
+ });
114
+ ```
115
+
116
+ 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.
117
+
118
+ ## Why emitting failure signals pays off
119
+
120
+ Here is a real failed login, from an app that emits both signals:
121
+
122
+ ```json
123
+ {
124
+ "verified": "no",
125
+ "verdict": {
126
+ "pass": false,
127
+ "failureReason": "no signal matched {\"kind\":\"signal\",\"name\":\"auth:granted\"}",
128
+ "observed": "signal 'auth:granted' never fired; signals seen in this window: auth:denied",
129
+ "expected": "signal 'auth:granted' to fire"
130
+ },
131
+ "summary": {
132
+ "net": { "total": 1, "errors": 1, "headline": "POST http://localhost:8787/api/login 401" },
133
+ "stateDiffs": [],
134
+ "signals": ["auth:denied"]
135
+ },
136
+ "honesty": { "grade": "signal", "integrity": { "clean": true } },
137
+ "capsule": {
138
+ "firstDivergence": {
139
+ "expected": { "kind": "signal", "name": "auth:granted" },
140
+ "observed": "signal \"auth:granted\" never fired"
141
+ },
142
+ "blastRadius": ["signal auth:denied"]
143
+ }
144
+ }
145
+ ```
146
+
147
+ Read `observed` again:
148
+
149
+ > signal 'auth:granted' never fired; **signals seen in this window: auth:denied**
150
+
151
+ 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.
152
+
153
+ 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.
154
+
155
+ <Note>
156
+ `capsule.firstDivergence` and `blastRadius` are saved to disk automatically on a failure, so the
157
+ evidence survives the agent's context window. Look for `capsuleSaved` in the response.
158
+ </Note>
159
+
160
+ ## When Reticle refuses to call it a pass
161
+
162
+ This is the one that surprises people, so here it is in full. A successful login, on a backgrounded tab:
163
+
164
+ ```json
165
+ {
166
+ "verified": "unknown",
167
+ "verifiedReason": "unsettled",
168
+ "because": "the page never settled, so the reaction window may have closed before the app finished",
169
+ "verdict": {
170
+ "pass": true,
171
+ "evidence": { "name": "auth:granted", "data": { "email": "admin@reticle.dev" } }
172
+ },
173
+ "summary": {
174
+ "stateDiffs": [{ "path": "auth", "from": null, "to": "{\"email\":\"admin@reticle.dev\"}" }],
175
+ "signals": ["auth:granted"]
176
+ },
177
+ "honesty": { "grade": "signal", "integrity": { "clean": true } },
178
+ "feedback_invite": "Reticle could not tell what happened here. If you expected otherwise, reticle_feedback. An unknown verdict is our defect, not yours"
179
+ }
180
+ ```
181
+
182
+ 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.
183
+
184
+ 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.
185
+
186
+ <Warning>
187
+ The usual cause is a **throttled tab**. A backgrounded browser tab has its timers suppressed,
188
+ which suppresses the quiescence detection Reticle uses to decide the page has settled. Focus the
189
+ tab, or drive a dedicated context. The response tells you: look for `session.throttled` and the
190
+ accompanying `warning`.
191
+ </Warning>
192
+
193
+ 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).
194
+
195
+ ## Testids, briefly
196
+
197
+ ```tsx
198
+ <button data-testid="login-submit">Sign in</button>
199
+ ```
200
+
201
+ 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.
202
+
203
+ ## Keeping it honest
204
+
205
+ 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.
206
+
207
+ ```bash
208
+ npm i -D @reticlehq/eslint-plugin
209
+ ```
210
+
211
+ 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.
212
+
213
+ ## Start with one flow
214
+
215
+ You do not need to describe the whole app, and trying to is the slow path that gets abandoned halfway.
216
+
217
+ 1. Pick your most important flow. The one that would be embarrassing to break.
218
+ 2. Register the store it reads.
219
+ 3. Emit a signal where it succeeds, and one where it fails.
220
+ 4. Add testids to the elements it touches.
221
+ 5. Drive it, and check `honesty.grade` says `signal`.
222
+
223
+ Then stop. Add more when a flow you actually replay needs it.
224
+
225
+ <CardGroup cols={2}>
226
+ <Card title="Prove it" icon="circle-check" href="/tools-act-and-wait">
227
+ Use your new signals as predicates and watch the grade climb.
228
+ </Card>
229
+ <Card title="Lock it in" icon="vial" href="/testing">
230
+ Turn the instrumented flow into a spec that runs on every pull request.
231
+ </Card>
232
+ </CardGroup>
@@ -1,24 +1,28 @@
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
+ ---
2
6
 
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.
7
+ 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
8
 
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)
9
+ - [1. Start here: reuse what you already have](#1-start-here-reuse-what-you-already-have)
10
+ - [2. Inject the emitter (zero prod bundle)](#2-inject-the-emitter-zero-prod-bundle)
11
+ - [3. Emit signals from the store layer, not N call sites](#3-emit-signals-from-the-store-layer-not-n-call-sites)
12
+ - [4. Self-registering domains (`registerReticleDomain`)](#4-self-registering-domains-registerreticledomain)
13
+ - [5. Keep the signal layer from rotting (`@reticlehq/eslint-plugin`)](#5-keep-the-signal-layer-from-rotting-reticleeslint-plugin)
14
+ - [6. Limitation: un-scriptable tabs, use `reticle drive`](#6-limitation-un-scriptable-tabs-use-reticle-drive)
11
15
  - [Checklist](#checklist)
12
16
 
13
17
  ---
14
18
 
15
- ## 1 — Start here: reuse what you already have
19
+ ## 1. Start here: reuse what you already have
16
20
 
17
21
  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
22
 
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.
23
+ **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
24
 
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.
25
+ **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
26
 
23
27
  ```ts
24
28
  import { registerCapabilities } from '@reticlehq/browser';
@@ -31,7 +35,7 @@ registerCapabilities({
31
35
  });
32
36
  ```
33
37
 
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.
38
+ **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
39
 
36
40
  ```ts
37
41
  import { reticle } from '@reticlehq/browser';
@@ -39,36 +43,36 @@ onSaved(() => reticle.signal('order:saved', { id, total }));
39
43
  // agent: reticle_assert({ predicate: { kind: 'signal', name: 'order:saved', dataMatches: { id: '*' } } })
40
44
  ```
41
45
 
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.
46
+ 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
47
 
44
- ## 2 — Inject the emitter (zero prod bundle)
48
+ ## 2. Inject the emitter (zero prod bundle)
45
49
 
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.**
50
+ 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
51
 
48
52
  ```ts
49
- // app/emit.ts — the one place that touches @reticlehq/browser
53
+ // app/emit.ts: the one place that touches @reticlehq/browser
50
54
  import { createReticleEmitter } from '@reticlehq/browser';
51
55
  export const emit = createReticleEmitter(); // no-op until reticle.connect()
52
56
  ```
53
57
 
54
58
  ```ts
55
- // any component — depends on the interface, not the SDK
59
+ // any component: depends on the interface, not the SDK
56
60
  import { emit } from '../emit';
57
61
  function onSaved(id: string, total: number) {
58
62
  emit.signal('order:saved', { id, total });
59
63
  }
60
64
  ```
61
65
 
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.
66
+ 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
67
 
64
- ## 3 — Emit signals from the store layer, not N call sites
68
+ ## 3. Emit signals from the store layer, not N call sites
65
69
 
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.
70
+ 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
71
 
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.
72
+ **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
73
 
70
74
  ```ts
71
- // store-with-reticle.ts — sketch: one audited transition map, not N call sites
75
+ // store-with-reticle.ts sketch: one audited transition map, not N call sites
72
76
  import { emit } from './emit';
73
77
 
74
78
  const signalFor: Record<string, (next: State) => readonly [string, Record<string, unknown>]> = {
@@ -83,7 +87,7 @@ function dispatch(action: keyof typeof signalFor, run: () => State): void {
83
87
  }
84
88
  ```
85
89
 
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.
90
+ **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
91
 
88
92
  ```ts
89
93
  import { commitAndSignal } from '@reticlehq/browser';
@@ -97,18 +101,18 @@ const next = commitAndSignal(
97
101
  );
98
102
  ```
99
103
 
100
- If `mutate` throws, the mutation never happened — so **the signal is not emitted and the error propagates** unchanged.
104
+ If `mutate` throws, the mutation never happened, so **the signal is not emitted and the error propagates** unchanged.
101
105
 
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.
106
+ > **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
107
 
104
108
  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
109
 
106
- ## 4 — Self-registering domains (`registerReticleDomain`)
110
+ ## 4. Self-registering domains (`registerReticleDomain`)
107
111
 
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.
112
+ 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
113
 
110
114
  ```ts
111
- // features/sections/reticle.ts — co-locate a domain's testids + signals in one module
115
+ // features/sections/reticle.ts: co-locate a domain's testids + signals in one module
112
116
  import { registerReticleDomain } from '@reticlehq/browser';
113
117
 
114
118
  export const SectionTestIds = { list: 'section-list', add: 'section-add' } as const;
@@ -127,11 +131,11 @@ import { registerReticleDomain } from '@reticlehq/browser';
127
131
  registerReticleDomain({ testids: ['search-input'], signals: ['search:ran'] });
128
132
  ```
129
133
 
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.")
134
+ 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
135
 
132
- ## 5 — Keep the signal layer from rotting (`@reticlehq/eslint-plugin`)
136
+ ## 5. Keep the signal layer from rotting (`@reticlehq/eslint-plugin`)
133
137
 
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.
138
+ 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
139
 
136
140
  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
141
 
@@ -154,11 +158,11 @@ export default [
154
158
 
155
159
  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.
156
160
 
157
- ## 6 — Limitation: un-scriptable tabs → `reticle drive`
161
+ ## 6. Limitation: un-scriptable tabs, use `reticle drive`
158
162
 
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`.
163
+ 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
164
 
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.
165
+ 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
166
 
163
167
  ## Checklist
164
168
 
@@ -1,8 +1,12 @@
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
+ > **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.
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
+ 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
10
 
7
11
  ## 1. Publish @reticlehq/\* to a local registry
8
12
 
@@ -16,7 +20,7 @@ This starts a **fresh** Verdaccio on `http://localhost:4873`, creates a user/tok
16
20
 
17
21
  | Package | What you install it for |
18
22
  | --- | --- |
19
- | **`@reticlehq/react`** | **install this** — the browser SDK kit you embed (re-exports the browser sensor, so one install gives both `reticle` and `install`) |
23
+ | **`@reticlehq/react`** | **install this**: the browser SDK kit you embed (re-exports the browser sensor, so one install gives both `reticle` and `install`) |
20
24
  | `@reticlehq/vite-plugin` | dev-only source mapping + `connect()` injection (Vite) |
21
25
  | `@reticlehq/next` | Next.js build wrapper (`withReticle`) |
22
26
  | `@reticlehq/server` | the bridge + MCP server (your agent runs it, `npx @reticlehq/server mcp`) |
@@ -47,18 +51,18 @@ npm i -D @reticlehq/react @reticlehq/vite-plugin
47
51
  # optional: npm i -D @reticlehq/eslint-plugin # require-signal-on-mutation lint rule
48
52
  ```
49
53
 
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).
54
+ 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
55
 
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`:
56
+ > **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 with `npm install @reticlehq/react@latest`:
53
57
  >
54
58
  > ```bash
55
59
  > npm i -D @reticlehq/react@latest @reticlehq/vite-plugin@latest @reticlehq/eslint-plugin@latest
56
60
  > ```
57
61
 
58
- **Run the MCP server** from the local registry too — `npx @reticlehq/server` _is_ the server:
62
+ **Run the MCP server** from the local registry too; `npx @reticlehq/server` _is_ the server:
59
63
 
60
64
  ```jsonc
61
- // .mcp.json — point npx at the local registry so it fetches @reticlehq/server from Verdaccio
65
+ // .mcp.json: point npx at the local registry so it fetches @reticlehq/server from Verdaccio
62
66
  {
63
67
  "mcpServers": {
64
68
  "reticle": {
@@ -86,7 +90,7 @@ Mount the SDK from a dev-only client component (see the Next.js section in [Gett
86
90
 
87
91
  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
92
 
89
- - **Easiest — `reticle drive`:** Reticle launches its own scriptable, headless-capable browser at your app URL (no flags to juggle):
93
+ - **Easiest (`reticle drive`):** Reticle launches its own scriptable, headless-capable browser at your app URL (no flags to juggle):
90
94
 
91
95
  ```bash
92
96
  npx --registry http://localhost:4873/ @reticlehq/server drive http://localhost:4310 # add --headed to watch
@@ -107,16 +111,16 @@ Synthetic events can't trigger native `onMouseEnter`/pointer state (hover menus,
107
111
  }
108
112
  ```
109
113
 
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).
114
+ 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
115
 
112
116
  ## Write replayable specs + git-checked flows
113
117
 
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).
118
+ - **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
119
  - **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
120
 
117
121
  ## When you're ready for real npm
118
122
 
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.
123
+ 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
124
 
121
125
  ## Cleanup
122
126
 
@@ -1,22 +1,26 @@
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
+ 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
8
 
5
9
  ## The mental model
6
10
 
7
11
  - **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.
12
+ - **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.
13
+ - **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.
10
14
  - **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).
11
15
 
12
- ## Manual testing — ~5 minutes
16
+ ## Manual testing: ~5 minutes
13
17
 
14
18
  1. Add the plugin (Vite/Next) or one `reticle.connect()` call. (See [getting-started](./getting-started.md).)
15
19
  2. Start your app as you normally do.
16
- 3. Open it in a browser — the in-page panel shows Reticle is connected.
20
+ 3. Open it in a browser; the in-page panel shows Reticle is connected.
17
21
  4. Click around; flag anything that looks wrong with the "Flag a bug" annotator. The agent drains those with `reticle_session {action:"review"}`.
18
22
 
19
- ## Agent testing — ~2 minutes
23
+ ## Agent testing: ~2 minutes
20
24
 
21
25
  With the app running and instrumented, an agent drives a flow end to end:
22
26
 
@@ -28,7 +32,7 @@ reticle_assert { sessionId, ... } # verify intent
28
32
  reticle_lease {action:"release"} { sessionId } # free the slot
29
33
  ```
30
34
 
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.
35
+ `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
36
 
33
37
  ## 10 agents, 10 flows, one dashboard
34
38
 
@@ -36,7 +40,7 @@ This is the design target, and it needs no special setup:
36
40
 
37
41
  - 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
42
  - 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`.
43
+ - Flows can't bleed into each other: contexts are isolated and every session is scoped by `projectId`.
40
44
  - 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
45
 
42
46
  `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 +50,10 @@ This is the design target, and it needs no special setup:
46
50
  | Env | Default | Effect |
47
51
  | --- | --- | --- |
48
52
  | `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). |
53
+ | `RETICLE_PORT` | from `.reticle.json`, else `4400` | Daemon port (rarely needed; discovery handles it). |
50
54
 
51
55
  ## Why not just open many browsers?
52
56
 
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.
57
+ 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
58
 
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.
59
+ **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.
@@ -0,0 +1,70 @@
1
+ ---
2
+ title: '@reticlehq/babel-plugin'
3
+ description: A Babel plugin that stamps data-reticle-source on JSX host elements so Reticle can name the file a node came from.
4
+ icon: file-code
5
+ ---
6
+
7
+ `@reticlehq/babel-plugin` puts the source location back. React 19 dropped `_debugSource`, which is what source mapping used to rely on, so without this the tools can tell you the component name but not the file.
8
+
9
+ **Version 2.8.0. Apache 2.0. CommonJS. No runtime dependencies. Peer dependency: `@babel/core ^7`.**
10
+
11
+ ## What it stamps
12
+
13
+ ```jsx
14
+ <button onClick={submit}>Sign in</button>
15
+ ```
16
+
17
+ becomes, in dev only:
18
+
19
+ ```jsx
20
+ <button onClick={submit} data-reticle-source="src/components/Login.tsx:81:6">
21
+ Sign in
22
+ </button>
23
+ ```
24
+
25
+ The path is relative to `process.cwd()`, with backslashes normalized to forward slashes. A file with no name stamps `unknown`.
26
+
27
+ ## What it skips
28
+
29
+ - Component tags. Only lowercase host elements are stamped, because a component's own JSX gets stamped where it is defined.
30
+ - Elements that already carry the attribute.
31
+ - Nodes with no location information.
32
+ - Anything whose tag name is not a plain `JSXIdentifier`.
33
+
34
+ ## Do you install it?
35
+
36
+ Only if you are wiring Babel yourself. [`@reticlehq/vite-plugin`](/packages/vite-plugin) and [`@reticlehq/next`](/packages/next) both pull it in.
37
+
38
+ ```bash
39
+ npm i -D @reticlehq/babel-plugin
40
+ ```
41
+
42
+ ## The export
43
+
44
+ One export, via `export =`. There are no named exports and no `default` interop wrapper, because Babel `require()`s the module and takes the object directly.
45
+
46
+ ```ts
47
+ function reticleSourcePlugin({ types }: { types: typeof BabelTypes }): PluginObj<PluginPass>;
48
+ ```
49
+
50
+ The Babel plugin name is `reticle-source`. Its visitor handles `JSXOpeningElement` only.
51
+
52
+ **It takes no options.** The plugin reads nothing from `state.opts`.
53
+
54
+ ## Wiring it by hand
55
+
56
+ ```js
57
+ // babel.config.cjs
58
+ module.exports = {
59
+ plugins: process.env.NODE_ENV === 'development' ? [require('@reticlehq/babel-plugin')] : [],
60
+ };
61
+ ```
62
+
63
+ <Warning>
64
+ Do not write `require('@reticlehq/babel-plugin').default`. With `export =` there is no `.default`,
65
+ and an old README example got this wrong.
66
+ </Warning>
67
+
68
+ ## Why an attribute rather than a runtime lookup
69
+
70
+ The stamp survives minification of everything else, costs nothing at runtime, and is readable from the DOM node itself. That is what lets [`reticle_inspect`](/tools-inspect) answer with a file and a line rather than a component name and a shrug.