@reticlehq/server 2.6.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.
- package/README.md +14 -5
- package/SKILL.md +251 -33
- package/dist/bridge/bridge.d.ts +6 -1
- package/dist/bridge/bridge.js +27 -4
- package/dist/capsule/causal-summary.d.ts +20 -0
- package/dist/capsule/causal-summary.js +31 -6
- package/dist/capsule/divergence.js +7 -1
- package/dist/cli/chromium-hint.d.ts +68 -0
- package/dist/cli/chromium-hint.js +145 -0
- package/dist/cli/cli-doctor.js +31 -36
- package/dist/cli/cli-kill.d.ts +58 -0
- package/dist/cli/cli-kill.js +162 -0
- package/dist/cli/cli-launch.d.ts +18 -0
- package/dist/cli/cli-launch.js +32 -1
- package/dist/cli/cli-parse.d.ts +16 -1
- package/dist/cli/cli-parse.js +32 -9
- package/dist/cli/cli-port.d.ts +8 -0
- package/dist/cli/cli-port.js +36 -1
- package/dist/cli/cli-verify.js +2 -2
- package/dist/cli/doctor-daemon-line.js +10 -1
- package/dist/cli/doctor-sessions-line.d.ts +12 -0
- package/dist/cli/doctor-sessions-line.js +66 -0
- package/dist/cli/port-holder.d.ts +10 -0
- package/dist/cli/port-holder.js +26 -2
- package/dist/cli.js +139 -23
- package/dist/crawl/crawl-tools.js +12 -2
- package/dist/crawl/crawl.d.ts +13 -0
- package/dist/crawl/crawl.js +50 -2
- package/dist/daemon/daemon-resilience.js +2 -2
- package/dist/daemon/loopback-alias.d.ts +32 -0
- package/dist/daemon/loopback-alias.js +80 -0
- package/dist/events/contradictions.d.ts +14 -0
- package/dist/events/contradictions.js +26 -17
- package/dist/events/predicate-eval.d.ts +26 -3
- package/dist/events/predicate-eval.js +284 -95
- package/dist/events/predicate-parse.d.ts +1 -1
- package/dist/events/predicate-parse.js +18 -4
- package/dist/events/predicate.js +170 -18
- package/dist/events/ring-buffer.d.ts +8 -0
- package/dist/events/ring-buffer.js +34 -1
- package/dist/flows/flow-classify.js +1 -1
- package/dist/flows/flow-tools.js +12 -4
- package/dist/flows/heal-run.js +1 -1
- package/dist/honesty/blind-spots.js +6 -2
- package/dist/honesty/honesty.d.ts +11 -0
- package/dist/honesty/honesty.js +9 -1
- package/dist/honesty/unsettled.d.ts +40 -0
- package/dist/honesty/unsettled.js +78 -0
- package/dist/honesty/verified.d.ts +7 -0
- package/dist/honesty/verified.js +27 -4
- package/dist/http-server.js +11 -1
- package/dist/index.js +29 -2
- package/dist/init/agent-rules.d.ts +16 -0
- package/dist/init/agent-rules.js +96 -17
- package/dist/init/astro-patch.d.ts +2 -1
- package/dist/init/astro-patch.js +11 -5
- package/dist/init/capabilities.d.ts +22 -0
- package/dist/init/capabilities.js +76 -0
- package/dist/init/confirm.d.ts +96 -0
- package/dist/init/confirm.js +138 -0
- package/dist/init/cra.js +3 -2
- package/dist/init/detect.d.ts +8 -0
- package/dist/init/detect.js +15 -0
- package/dist/init/non-js-project.d.ts +27 -0
- package/dist/init/non-js-project.js +84 -0
- package/dist/init/plan-framework.d.ts +9 -0
- package/dist/init/plan-framework.js +72 -16
- package/dist/init/plan.d.ts +40 -4
- package/dist/init/plan.js +183 -43
- package/dist/init/run.d.ts +24 -2
- package/dist/init/run.js +181 -46
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +46 -6
- package/dist/init/snippets.d.ts +64 -5
- package/dist/init/snippets.js +220 -31
- package/dist/init/vite-config.js +29 -3
- package/dist/input/network-mock-tools.d.ts +19 -0
- package/dist/input/network-mock-tools.js +29 -4
- package/dist/license/license.js +1 -1
- package/dist/mcp/mcp-proxy.d.ts +69 -3
- package/dist/mcp/mcp-proxy.js +215 -23
- package/dist/mcp/mcp.js +58 -7
- package/dist/mcp/proxy-lifecycle.d.ts +20 -1
- package/dist/mcp/proxy-lifecycle.js +22 -1
- package/dist/pool/browser-pool.d.ts +22 -2
- package/dist/pool/browser-pool.js +44 -4
- package/dist/pool/playwright-launcher.js +10 -2
- package/dist/project/learned-routes.d.ts +24 -0
- package/dist/project/learned-routes.js +63 -0
- package/dist/project/project-store.d.ts +6 -0
- package/dist/project/project-store.js +25 -1
- package/dist/session/live-control-tools.js +39 -2
- package/dist/session/live-control.d.ts +7 -9
- package/dist/session/live-control.js +34 -1
- package/dist/session/no-session-diagnosis.d.ts +23 -4
- package/dist/session/no-session-diagnosis.js +128 -44
- package/dist/session/no-session-watch.js +12 -0
- package/dist/session/ref-provenance.d.ts +54 -0
- package/dist/session/ref-provenance.js +61 -0
- package/dist/session/review-store.d.ts +19 -0
- package/dist/session/review-store.js +34 -5
- package/dist/session/session-lease.js +1 -1
- package/dist/session/session-manager.d.ts +9 -0
- package/dist/session/session-manager.js +11 -0
- package/dist/session/session.d.ts +8 -0
- package/dist/session/session.js +12 -0
- package/dist/status-payload.d.ts +13 -1
- package/dist/status-payload.js +6 -1
- package/dist/telemetry/app-instrumented.d.ts +37 -0
- package/dist/telemetry/app-instrumented.js +54 -0
- package/dist/telemetry/automation-hint.d.ts +17 -0
- package/dist/telemetry/automation-hint.js +71 -0
- package/dist/telemetry/bug-found.d.ts +0 -20
- package/dist/telemetry/bug-found.js +38 -21
- package/dist/telemetry/cli-telemetry.js +7 -1
- package/dist/telemetry/daemon-telemetry.d.ts +4 -4
- package/dist/telemetry/daemon-telemetry.js +8 -4
- package/dist/telemetry/feedback.js +1 -1
- package/dist/telemetry/init-telemetry.js +9 -1
- package/dist/telemetry/install-source.d.ts +36 -0
- package/dist/telemetry/install-source.js +40 -0
- package/dist/telemetry/run-telemetry.js +4 -1
- package/dist/telemetry/session-metrics.d.ts +3 -3
- package/dist/telemetry/session-metrics.js +9 -4
- package/dist/telemetry/telemetry.d.ts +7 -1
- package/dist/telemetry/telemetry.js +17 -2
- package/dist/telemetry/tool-refused.d.ts +19 -0
- package/dist/telemetry/tool-refused.js +55 -0
- package/dist/telemetry/verification-of.js +72 -8
- package/dist/temp-dir.d.ts +27 -0
- package/dist/temp-dir.js +46 -0
- package/dist/tools/act-tools.js +77 -11
- package/dist/tools/assert-grade.d.ts +1 -1
- package/dist/tools/assert-grade.js +27 -5
- package/dist/tools/assert-verdict.js +25 -1
- package/dist/tools/contract-tools.js +4 -0
- package/dist/tools/dynamic-tools.js +1 -1
- package/dist/tools/error-recovery.d.ts +10 -0
- package/dist/tools/error-recovery.js +70 -5
- package/dist/tools/feedback-invite.js +1 -1
- package/dist/tools/invoke-tool.js +149 -32
- package/dist/tools/lease-tools.d.ts +29 -1
- package/dist/tools/lease-tools.js +58 -5
- package/dist/tools/navigate-result.js +1 -1
- package/dist/tools/observe-tools.js +1 -1
- package/dist/tools/observed-nothing.js +1 -1
- package/dist/tools/react-grace.d.ts +48 -0
- package/dist/tools/react-grace.js +87 -0
- package/dist/tools/read-tools.js +20 -4
- package/dist/tools/settle-in-flight.d.ts +79 -0
- package/dist/tools/settle-in-flight.js +106 -0
- package/dist/tools/snapshot-delta.d.ts +7 -0
- package/dist/tools/snapshot-delta.js +130 -6
- package/dist/tools/tool-kit.d.ts +52 -5
- package/dist/tools/tool-kit.js +55 -24
- package/dist/tools/tool-surface.js +28 -9
- package/dist/tools/tools.js +38 -4
- package/dist/visual/capture-cleanup.d.ts +16 -0
- package/dist/visual/capture-cleanup.js +39 -0
- package/dist/visual/visual-tools.d.ts +18 -0
- package/dist/visual/visual-tools.js +24 -5
- package/docs/README.md +56 -11
- package/docs/actions.mdx +87 -0
- package/docs/agent-cheatsheet.md +102 -26
- package/docs/architecture.md +46 -35
- package/docs/benchmarks.md +76 -72
- package/docs/best-practices.mdx +211 -0
- package/docs/capabilities.mdx +387 -0
- package/docs/cli/affected.mdx +51 -0
- package/docs/cli/capsules.mdx +69 -0
- package/docs/cli/cloud.mdx +127 -0
- package/docs/cli/doctor.mdx +76 -0
- package/docs/cli/drive.mdx +55 -0
- package/docs/cli/feedback.mdx +67 -0
- package/docs/cli/gate.mdx +64 -0
- package/docs/cli/help.mdx +92 -0
- package/docs/cli/hunt.mdx +57 -0
- package/docs/cli/identify.mdx +62 -0
- package/docs/cli/init.mdx +94 -0
- package/docs/cli/kill.mdx +91 -0
- package/docs/cli/license.mdx +48 -0
- package/docs/cli/mcp.mdx +61 -0
- package/docs/cli/open.mdx +68 -0
- package/docs/cli/restart.mdx +60 -0
- package/docs/cli/rollback.mdx +35 -0
- package/docs/cli/serve.mdx +62 -0
- package/docs/cli/status.mdx +64 -0
- package/docs/cli/stop.mdx +56 -0
- package/docs/cli/telemetry.mdx +57 -0
- package/docs/cli/update.mdx +52 -0
- package/docs/cli/verify.mdx +57 -0
- package/docs/cli/version.mdx +39 -0
- package/docs/cli/watch.mdx +55 -0
- package/docs/cli.mdx +219 -0
- package/docs/debugging.md +13 -9
- package/docs/deploy-checks.md +15 -11
- package/docs/desktop-apps.md +49 -40
- package/docs/desktop.mdx +188 -0
- package/docs/docs.json +363 -0
- package/docs/enterprise.md +20 -16
- package/docs/faq.mdx +88 -0
- package/docs/fixtures.md +11 -7
- package/docs/flows.md +26 -22
- package/docs/for-agents.md +71 -0
- package/docs/frameworks.mdx +199 -0
- package/docs/gate-plan.md +45 -41
- package/docs/gates.md +23 -19
- package/docs/getting-started.md +76 -65
- package/docs/human-control.md +22 -18
- package/docs/index.mdx +101 -0
- package/docs/install-agentic.mdx +215 -0
- package/docs/install-manual.mdx +194 -0
- package/docs/instrumentation.mdx +232 -0
- package/docs/integration-patterns.md +38 -34
- package/docs/local-registry.md +16 -12
- package/docs/multi-agent-testing.md +16 -12
- package/docs/packages/babel-plugin.mdx +70 -0
- package/docs/packages/browser.mdx +111 -0
- package/docs/packages/core.mdx +106 -0
- package/docs/packages/electron.mdx +87 -0
- package/docs/packages/eslint-plugin.mdx +84 -0
- package/docs/packages/next.mdx +91 -0
- package/docs/packages/react.mdx +91 -0
- package/docs/packages/server.mdx +98 -0
- package/docs/packages/tauri.mdx +63 -0
- package/docs/packages/test.mdx +108 -0
- package/docs/packages/vite-plugin.mdx +96 -0
- package/docs/packages.mdx +217 -0
- package/docs/platform-integration.md +33 -26
- package/docs/predicates.mdx +179 -0
- package/docs/quickstart.mdx +211 -0
- package/docs/recipes.mdx +242 -0
- package/docs/skill-file.mdx +105 -0
- package/docs/state-management.mdx +197 -0
- package/docs/style.css +14 -0
- package/docs/system-map.md +47 -32
- package/docs/telemetry-contract.md +101 -46
- package/docs/telemetry-events.mdx +430 -0
- package/docs/telemetry.md +63 -50
- package/docs/testing.md +10 -6
- package/docs/token-efficiency.md +30 -26
- package/docs/tools-act-and-wait.mdx +123 -0
- package/docs/tools-act-sequence.mdx +120 -0
- package/docs/tools-act.mdx +82 -0
- package/docs/tools-assert.mdx +70 -0
- package/docs/tools-console.mdx +59 -0
- package/docs/tools-inspect.mdx +85 -0
- package/docs/tools-navigate.mdx +63 -0
- package/docs/tools-network.mdx +86 -0
- package/docs/tools-observe.mdx +89 -0
- package/docs/tools-overview.mdx +157 -0
- package/docs/tools-query.mdx +82 -0
- package/docs/tools-session-and-feedback.mdx +91 -0
- package/docs/tools-sessions.mdx +65 -0
- package/docs/tools-snapshot.mdx +75 -0
- package/docs/tools-state.mdx +81 -0
- package/docs/tools-tools-and-run.mdx +125 -0
- package/docs/tools-wait-for.mdx +66 -0
- package/docs/troubleshooting.mdx +193 -0
- package/docs/usage.md +175 -153
- package/docs/vs-chrome-devtools-mcp.mdx +82 -0
- package/docs/vs-playwright-mcp.mdx +85 -0
- package/docs/vs-screenshots.mdx +96 -0
- package/docs/what-is-reticle.mdx +63 -0
- package/docs/why-reticle.mdx +107 -0
- package/package.json +2 -2
- package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
- package/docs/matrix/2.5.0/codex-machine.json +0 -20
- package/docs/matrix/2.5.0/cursor-machine.json +0 -22
- package/docs/matrix/2.5.0/gemini-machine.json +0 -22
- package/docs/matrix/2.5.0/opencode-machine.json +0 -22
- package/docs/matrix/2.5.0/vscode-machine.json +0 -22
- package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
- package/docs/matrix/MATRIX.md +0 -26
- package/docs/matrix/README.md +0 -59
- 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
|
-
|
|
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
|
|
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
|
|
6
|
-
- [2
|
|
7
|
-
- [3
|
|
8
|
-
- [4
|
|
9
|
-
- [5
|
|
10
|
-
- [6
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
package/docs/local-registry.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
62
|
+
**Run the MCP server** from the local registry too; `npx @reticlehq/server` _is_ the server:
|
|
59
63
|
|
|
60
64
|
```jsonc
|
|
61
|
-
// .mcp.json
|
|
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
|
|
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
|
|
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(...) })
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|