@reticlehq/server 2.7.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -5
- package/SKILL.md +130 -778
- package/dist/bridge/bridge.d.ts +23 -0
- package/dist/bridge/bridge.js +80 -2
- package/dist/bridge/bridge.test-harness.d.ts +5 -0
- package/dist/bridge/bridge.test-harness.js +6 -4
- package/dist/capsule/causal-summary.d.ts +36 -1
- package/dist/capsule/causal-summary.js +33 -7
- package/dist/capsule/divergence.js +7 -1
- package/dist/cli/chromium-hint.d.ts +33 -5
- package/dist/cli/chromium-hint.js +82 -12
- package/dist/cli/cli-doctor.js +65 -34
- package/dist/cli/cli-flow-commands.js +8 -8
- package/dist/cli/cli-kill.d.ts +58 -0
- package/dist/cli/cli-kill.js +162 -0
- package/dist/cli/cli-parse.d.ts +16 -1
- package/dist/cli/cli-parse.js +32 -9
- package/dist/cli/cli-port.d.ts +32 -0
- package/dist/cli/cli-port.js +82 -35
- package/dist/cli/cli-verify.d.ts +21 -0
- package/dist/cli/cli-verify.js +47 -3
- package/dist/cli/config-discovery.d.ts +35 -0
- package/dist/cli/config-discovery.js +163 -0
- package/dist/cli/doctor-daemon-line.js +4 -1
- package/dist/cli/doctor-project-line.d.ts +37 -0
- package/dist/cli/doctor-project-line.js +35 -0
- package/dist/cli/doctor-rows.d.ts +51 -0
- package/dist/cli/doctor-rows.js +54 -0
- package/dist/cli/doctor-sessions-line.d.ts +12 -0
- package/dist/cli/doctor-sessions-line.js +69 -0
- package/dist/cli/drive-attach.d.ts +99 -0
- package/dist/cli/drive-attach.js +203 -0
- package/dist/cli/drive-command.d.ts +13 -0
- package/dist/cli/drive-command.js +103 -0
- package/dist/cli/open-note.d.ts +20 -0
- package/dist/cli/open-note.js +37 -0
- package/dist/cli/port-holder.d.ts +10 -0
- package/dist/cli/port-holder.js +26 -2
- package/dist/cli/status-next-action.d.ts +37 -0
- package/dist/cli/status-next-action.js +61 -0
- package/dist/cli.js +119 -39
- 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/events/contradictions.d.ts +46 -0
- package/dist/events/contradictions.js +35 -22
- package/dist/events/declared.d.ts +45 -0
- package/dist/events/declared.js +73 -0
- package/dist/events/echo-mismatch.d.ts +8 -2
- package/dist/events/echo-mismatch.js +33 -5
- package/dist/events/event-filters.d.ts +1 -1
- package/dist/events/event-filters.js +7 -4
- package/dist/events/predicate-eval.d.ts +17 -72
- package/dist/events/predicate-eval.js +117 -236
- package/dist/events/predicate-parse.js +31 -3
- package/dist/events/predicate-route.d.ts +32 -0
- package/dist/events/predicate-route.js +102 -0
- package/dist/events/predicate-schema.d.ts +99 -0
- package/dist/events/predicate-schema.js +309 -0
- package/dist/events/predicate.d.ts +8 -0
- package/dist/events/predicate.js +296 -22
- package/dist/flows/annotate-tools.js +3 -0
- package/dist/flows/annotate.js +18 -4
- package/dist/flows/coverage.d.ts +38 -5
- package/dist/flows/coverage.js +30 -4
- package/dist/flows/decision.d.ts +15 -0
- package/dist/flows/decision.js +1 -1
- package/dist/flows/flow-classify.js +1 -1
- package/dist/flows/flow-replay-run.js +8 -1
- package/dist/flows/flow-tools.js +21 -5
- package/dist/flows/heal-run.js +1 -1
- package/dist/flows/replay.js +3 -0
- package/dist/honesty/accepted-write.d.ts +3 -0
- package/dist/honesty/accepted-write.js +8 -3
- package/dist/honesty/already-true.js +15 -0
- package/dist/honesty/blind-spots.js +10 -2
- package/dist/honesty/honesty.d.ts +11 -0
- package/dist/honesty/honesty.js +1 -0
- package/dist/honesty/uncaptured-bodies.d.ts +4 -2
- package/dist/honesty/uncaptured-bodies.js +9 -2
- package/dist/honesty/unread-outcome.d.ts +8 -0
- package/dist/honesty/unread-outcome.js +36 -9
- package/dist/honesty/unsettled.d.ts +49 -0
- package/dist/honesty/unsettled.js +82 -0
- package/dist/honesty/verified.d.ts +36 -3
- package/dist/honesty/verified.js +54 -11
- package/dist/http-server.d.ts +19 -0
- package/dist/http-server.js +202 -4
- package/dist/impact/impact-recorder.d.ts +23 -0
- package/dist/impact/impact-recorder.js +76 -0
- package/dist/impact/impact-store.d.ts +47 -0
- package/dist/impact/impact-store.js +171 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +47 -10
- package/dist/init/agent-rules.d.ts +36 -0
- package/dist/init/agent-rules.js +132 -22
- package/dist/init/astro-patch.d.ts +2 -1
- package/dist/init/astro-patch.js +38 -6
- package/dist/init/capabilities.d.ts +22 -0
- package/dist/init/capabilities.js +76 -0
- package/dist/init/confirm.d.ts +114 -0
- package/dist/init/confirm.js +165 -0
- package/dist/init/connect-steps.d.ts +2 -0
- package/dist/init/connect-steps.js +50 -0
- package/dist/init/cra.js +3 -2
- package/dist/init/csp-check.d.ts +38 -0
- package/dist/init/csp-check.js +92 -0
- package/dist/init/csp-doctor.d.ts +25 -0
- package/dist/init/csp-doctor.js +49 -0
- package/dist/init/cursor.js +6 -9
- package/dist/init/detect.d.ts +8 -0
- package/dist/init/detect.js +33 -3
- package/dist/init/mcp-clients.d.ts +1 -0
- package/dist/init/mcp-clients.js +51 -7
- package/dist/init/mcp.d.ts +15 -0
- package/dist/init/mcp.js +19 -0
- package/dist/init/non-js-project.js +29 -5
- package/dist/init/plan-framework.d.ts +10 -0
- package/dist/init/plan-framework.js +78 -16
- package/dist/init/plan.d.ts +47 -6
- package/dist/init/plan.js +228 -69
- package/dist/init/run.d.ts +24 -2
- package/dist/init/run.js +157 -45
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +54 -6
- package/dist/init/snippets.d.ts +45 -5
- package/dist/init/snippets.js +194 -42
- package/dist/init/vite-config.js +53 -5
- package/dist/input/network-mock-tools.d.ts +19 -0
- package/dist/input/network-mock-tools.js +37 -5
- package/dist/input/scroll-find.d.ts +6 -0
- package/dist/input/scroll-find.js +17 -2
- package/dist/input/scroll-tools.js +4 -0
- package/dist/journal/attach-journal.d.ts +9 -1
- package/dist/journal/attach-journal.js +13 -2
- package/dist/journal/journal-query.d.ts +13 -0
- package/dist/journal/journal-query.js +28 -3
- package/dist/journal/workspace-gitignore.d.ts +10 -0
- package/dist/journal/workspace-gitignore.js +59 -0
- package/dist/license/license.js +1 -1
- package/dist/mcp/mcp-proxy.d.ts +35 -30
- package/dist/mcp/mcp-proxy.js +306 -104
- package/dist/mcp/mcp.d.ts +10 -1
- package/dist/mcp/mcp.js +70 -34
- package/dist/mcp/proxy-handshake.d.ts +82 -1
- package/dist/mcp/proxy-handshake.js +109 -2
- package/dist/mcp/proxy-lifecycle.d.ts +11 -2
- package/dist/mcp/proxy-lifecycle.js +12 -3
- package/dist/mcp/proxy-log.d.ts +63 -0
- package/dist/mcp/proxy-log.js +132 -0
- package/dist/mcp/server-instructions.d.ts +17 -0
- package/dist/mcp/server-instructions.js +56 -0
- package/dist/mcp/shared-params.d.ts +27 -0
- package/dist/mcp/shared-params.js +36 -0
- package/dist/pool/browser-pool.d.ts +22 -2
- package/dist/pool/browser-pool.js +44 -4
- 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/project/reticle-dir.d.ts +2 -0
- package/dist/project/reticle-dir.js +1 -0
- package/dist/session/connection-memory.d.ts +59 -0
- package/dist/session/connection-memory.js +108 -0
- package/dist/session/dev-command.d.ts +26 -0
- package/dist/session/dev-command.js +89 -0
- package/dist/session/dev-server-probe.d.ts +68 -0
- package/dist/session/dev-server-probe.js +102 -7
- package/dist/session/last-act.d.ts +26 -0
- package/dist/session/last-act.js +35 -1
- package/dist/session/live-control-tools.js +42 -5
- package/dist/session/no-session-diagnosis.d.ts +67 -2
- package/dist/session/no-session-diagnosis.js +236 -27
- package/dist/session/no-session-next-action.d.ts +46 -0
- package/dist/session/no-session-next-action.js +85 -0
- package/dist/session/no-session-watch.d.ts +40 -1
- package/dist/session/no-session-watch.js +174 -8
- package/dist/session/pending-commands.d.ts +12 -0
- package/dist/session/pending-commands.js +13 -1
- package/dist/session/ref-provenance.d.ts +54 -0
- package/dist/session/ref-provenance.js +61 -0
- package/dist/session/session-lease.js +1 -1
- package/dist/session/session-manager.d.ts +4 -0
- package/dist/session/session-manager.js +35 -5
- package/dist/session/session.d.ts +10 -1
- package/dist/session/session.js +146 -5
- package/dist/telemetry/app-instrumented.d.ts +8 -0
- package/dist/telemetry/app-instrumented.js +10 -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 +15 -17
- package/dist/telemetry/bug-found.js +52 -21
- package/dist/telemetry/cli-telemetry.js +7 -1
- package/dist/telemetry/daemon-telemetry.js +24 -0
- package/dist/telemetry/feedback.js +1 -1
- package/dist/telemetry/init-telemetry.js +9 -1
- package/dist/telemetry/install-source.d.ts +56 -0
- package/dist/telemetry/install-source.js +74 -0
- package/dist/telemetry/instrumentation-stall.d.ts +50 -0
- package/dist/telemetry/instrumentation-stall.js +90 -0
- package/dist/telemetry/run-telemetry.js +4 -1
- package/dist/telemetry/session-metrics.d.ts +8 -0
- package/dist/telemetry/session-metrics.js +15 -0
- package/dist/telemetry/telemetry.d.ts +7 -1
- package/dist/telemetry/telemetry.js +26 -2
- package/dist/telemetry/tool-refused.d.ts +19 -0
- package/dist/telemetry/tool-refused.js +55 -0
- package/dist/telemetry/verification-of.js +46 -7
- package/dist/temp-dir.d.ts +27 -0
- package/dist/temp-dir.js +46 -0
- package/dist/tools/act-tools.js +209 -28
- package/dist/tools/assert-grade.d.ts +1 -1
- package/dist/tools/assert-grade.js +27 -5
- package/dist/tools/assert-verdict.js +49 -7
- package/dist/tools/browser-tools.js +10 -0
- package/dist/tools/contract-tools.js +4 -0
- package/dist/tools/dynamic-tools.js +16 -7
- package/dist/tools/error-recovery.d.ts +10 -0
- package/dist/tools/error-recovery.js +73 -9
- package/dist/tools/feedback-invite.js +1 -1
- package/dist/tools/invoke-tool.js +159 -26
- package/dist/tools/lease-hint.d.ts +30 -8
- package/dist/tools/lease-hint.js +65 -14
- package/dist/tools/lease-tools.d.ts +40 -1
- package/dist/tools/lease-tools.js +181 -83
- package/dist/tools/navigate-result.js +1 -1
- package/dist/tools/observe-tools.js +10 -4
- package/dist/tools/observed-nothing.js +1 -1
- package/dist/tools/react-grace.d.ts +52 -0
- package/dist/tools/react-grace.js +90 -0
- package/dist/tools/read-tools.js +67 -5
- package/dist/tools/resolve-target.d.ts +33 -0
- package/dist/tools/resolve-target.js +53 -0
- package/dist/tools/settle-in-flight.d.ts +94 -0
- package/dist/tools/settle-in-flight.js +146 -0
- package/dist/tools/snapshot-delta.d.ts +10 -0
- package/dist/tools/snapshot-delta.js +194 -9
- package/dist/tools/tool-kit.d.ts +8 -0
- package/dist/tools/tool-surface.d.ts +60 -0
- package/dist/tools/tool-surface.js +86 -1
- package/dist/tools/tools.js +61 -8
- package/dist/tools/unadvertised-help.js +9 -6
- package/docs/README.md +55 -11
- package/docs/actions.mdx +120 -0
- package/docs/agent-cheatsheet.md +95 -29
- package/docs/architecture.md +52 -35
- package/docs/benchmarks.md +127 -75
- package/docs/best-practices.mdx +216 -0
- package/docs/capabilities.mdx +534 -0
- package/docs/cli/affected.mdx +53 -0
- package/docs/cli/capsules.mdx +70 -0
- package/docs/cli/cloud.mdx +153 -0
- package/docs/cli/doctor.mdx +109 -0
- package/docs/cli/drive.mdx +74 -0
- package/docs/cli/feedback.mdx +67 -0
- package/docs/cli/gate.mdx +64 -0
- package/docs/cli/help.mdx +94 -0
- package/docs/cli/hunt.mdx +73 -0
- package/docs/cli/identify.mdx +76 -0
- package/docs/cli/init.mdx +121 -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 +74 -0
- package/docs/cli/restart.mdx +60 -0
- package/docs/cli/rollback.mdx +35 -0
- package/docs/cli/serve.mdx +69 -0
- package/docs/cli/status.mdx +88 -0
- package/docs/cli/stop.mdx +64 -0
- package/docs/cli/telemetry.mdx +57 -0
- package/docs/cli/update.mdx +52 -0
- package/docs/cli/verify.mdx +76 -0
- package/docs/cli/version.mdx +39 -0
- package/docs/cli/watch.mdx +62 -0
- package/docs/cli.mdx +241 -0
- package/docs/debugging.md +16 -10
- package/docs/deploy-checks.md +45 -12
- package/docs/desktop.mdx +384 -0
- package/docs/docs.json +364 -0
- package/docs/enterprise.md +22 -16
- package/docs/faq.mdx +102 -0
- package/docs/fixtures.md +13 -7
- package/docs/flows.md +29 -23
- package/docs/for-agents.md +71 -0
- package/docs/frameworks.mdx +238 -0
- package/docs/gate-plan.md +48 -42
- package/docs/gates.md +28 -22
- package/docs/getting-started.md +129 -77
- package/docs/human-control.md +24 -18
- package/docs/index.mdx +143 -0
- package/docs/install-agentic.mdx +224 -0
- package/docs/install-manual.mdx +196 -0
- package/docs/instrumentation.mdx +238 -0
- package/docs/integration-patterns.md +41 -35
- package/docs/local-registry.md +19 -13
- package/docs/multi-agent-testing.md +19 -13
- package/docs/packages/babel-plugin.mdx +79 -0
- package/docs/packages/browser.mdx +111 -0
- package/docs/packages/core.mdx +116 -0
- package/docs/packages/electron.mdx +85 -0
- package/docs/packages/eslint-plugin.mdx +86 -0
- package/docs/packages/next.mdx +89 -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 +119 -0
- package/docs/packages/vite-plugin.mdx +94 -0
- package/docs/packages.mdx +218 -0
- package/docs/platform-integration.md +35 -26
- package/docs/predicates.mdx +248 -0
- package/docs/quickstart.mdx +257 -0
- package/docs/recipes.mdx +282 -0
- package/docs/skill-file.mdx +107 -0
- package/docs/state-management.mdx +199 -0
- package/docs/style.css +14 -0
- package/docs/system-map.md +50 -33
- package/docs/telemetry-contract.md +106 -52
- package/docs/telemetry-events.mdx +475 -0
- package/docs/telemetry.md +63 -55
- package/docs/testing.md +44 -6
- package/docs/token-efficiency.md +37 -27
- package/docs/tools-act-and-wait.mdx +173 -0
- package/docs/tools-act-sequence.mdx +137 -0
- package/docs/tools-act.mdx +95 -0
- package/docs/tools-assert.mdx +129 -0
- package/docs/tools-console.mdx +63 -0
- package/docs/tools-inspect.mdx +88 -0
- package/docs/tools-navigate.mdx +99 -0
- package/docs/tools-network.mdx +119 -0
- package/docs/tools-observe.mdx +145 -0
- package/docs/tools-overview.mdx +160 -0
- package/docs/tools-query.mdx +126 -0
- package/docs/tools-session-and-feedback.mdx +112 -0
- package/docs/tools-sessions.mdx +94 -0
- package/docs/tools-snapshot.mdx +129 -0
- package/docs/tools-state.mdx +104 -0
- package/docs/tools-tools-and-run.mdx +156 -0
- package/docs/tools-wait-for.mdx +85 -0
- package/docs/troubleshooting.mdx +263 -0
- package/docs/usage.md +209 -165
- package/docs/vs-chrome-devtools-mcp.mdx +123 -0
- package/docs/vs-playwright-mcp.mdx +125 -0
- package/docs/vs-screenshots.mdx +137 -0
- package/docs/what-is-reticle.mdx +63 -0
- package/docs/why-reticle.mdx +109 -0
- package/package.json +5 -5
- package/docs/desktop-apps.md +0 -256
- 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
package/docs/debugging.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Debugging Reticle
|
|
3
|
+
description: For people working on Reticle itself. How to work out why a flow behaved the way it did.
|
|
4
|
+
icon: bug
|
|
5
|
+
---
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
**To debug Reticle itself, start with the daemon log at `~/.reticle/daemon-<port>.log`, and turn on `RETICLE_TRACE=1` for the daemon when you need per-stage timings.** `reticle doctor` prints the exact log path for your port. Reticle produces four distinct signals and they answer four different questions, so the first step is picking the right one.
|
|
8
|
+
|
|
9
|
+
> For people working **on** Reticle, and for an agent asked to explain why a flow behaved the way it did. If you are debugging **your app** with Reticle, you want [usage.md](usage.md) instead.
|
|
4
10
|
|
|
5
11
|
Reticle produces four different signals and they answer four different questions. Reaching for the wrong one is why investigations here have historically started by reading source.
|
|
6
12
|
|
|
@@ -27,7 +33,7 @@ tail -f ~/.reticle/daemon-4400.log | jq .
|
|
|
27
33
|
|
|
28
34
|
## Verbose flow tracing
|
|
29
35
|
|
|
30
|
-
Off by default, because a trace on every tool call is a cost on the hot path
|
|
36
|
+
Off by default, because a trace on every tool call is a cost on the hot path, and a verification loop is 50 to 200 calls. Measured, so the claim is checkable: a disabled span costs **126ns** per site (against ~9ns for a bare call), which is under a microsecond per tool call and not the literal zero it is tempting to write. Turn it on **for the daemon** (the flag is read per call, but the daemon is the process doing the work, so it has to be set where the daemon starts):
|
|
31
37
|
|
|
32
38
|
```bash
|
|
33
39
|
RETICLE_TRACE=1 reticle serve --port 4400
|
|
@@ -35,17 +41,17 @@ RETICLE_TRACE=1 reticle serve --port 4400
|
|
|
35
41
|
|
|
36
42
|
Every instrumented stage then emits one line **when it ends**, carrying its own duration:
|
|
37
43
|
|
|
38
|
-
```
|
|
44
|
+
```jsonl
|
|
39
45
|
{"event":"trace","span":"browser.command","ms":412,"depth":1,"callId":"p8123-c7","ok":true,"command":"act"}
|
|
40
46
|
{"event":"trace","span":"tool.handler","ms":430,"depth":0,"callId":"p8123-c7","ok":true,"tool":"reticle_act"}
|
|
41
47
|
```
|
|
42
48
|
|
|
43
49
|
Read it like this:
|
|
44
50
|
|
|
45
|
-
- **`callId`** groups every stage of one tool call, and is prefixed with the daemon's pid so two daemons (or one that restarted) can never claim the same id. Several agents can be inside the daemon at once, so lines interleave
|
|
51
|
+
- **`callId`** groups every stage of one tool call, and is prefixed with the daemon's pid so two daemons (or one that restarted) can never claim the same id. Several agents can be inside the daemon at once, so lines interleave; the id is the only thing that makes the output a tree instead of a pile.
|
|
46
52
|
- **`depth`** is the nesting level. `0` is the tool handler; anything deeper ran inside it.
|
|
47
53
|
- **`ms`** is that stage's own wall-clock. A parent's `ms` includes its children's.
|
|
48
|
-
- **`ok:false`** carries an `error` field. A stage that threw is still traced
|
|
54
|
+
- **`ok:false`** carries an `error` field. A stage that threw is still traced; otherwise the trace would show a call that entered a stage and never left, which reads as a hang.
|
|
49
55
|
|
|
50
56
|
One line per stage, at the end, is deliberate: a start line as well would double the volume to say something the end line already implies.
|
|
51
57
|
|
|
@@ -61,7 +67,7 @@ The spans that ship today:
|
|
|
61
67
|
| `crawl.step` | one control the crawl clicked, including its settle budget |
|
|
62
68
|
| `init.plan` / `init.apply` / `init.exec` / `init.exec.retry` / `init.write` | the install, phase by phase |
|
|
63
69
|
|
|
64
|
-
The first two answer _"is this slow because of us or because of the app under test?"_
|
|
70
|
+
The first two answer _"is this slow because of us or because of the app under test?"_
|
|
65
71
|
|
|
66
72
|
`browser.command` close to `tool.handler` means the app is taking the time. A large gap between them is Reticle's own overhead, and that is a performance bug of ours.
|
|
67
73
|
|
|
@@ -79,7 +85,7 @@ grep '"event":"trace"' ~/.reticle/daemon-4400.log | jq -sc 'sort_by(-.ms)[:10] |
|
|
|
79
85
|
|
|
80
86
|
### The init flow
|
|
81
87
|
|
|
82
|
-
`reticle init` is synchronous end to end, so it uses `spanSync
|
|
88
|
+
`reticle init` is synchronous end to end, so it uses `spanSync`: same line, same tree:
|
|
83
89
|
|
|
84
90
|
```
|
|
85
91
|
init.plan 2ms
|
|
@@ -89,7 +95,7 @@ init.write 1ms target=.reticle.json
|
|
|
89
95
|
init.apply 262ms steps=5
|
|
90
96
|
```
|
|
91
97
|
|
|
92
|
-
Two things that trace makes visible and nothing else did. Planning is ~2ms, so init's wall-clock is essentially all subprocess: the package manager, and `claude mcp add` when MCP registration is on. And when the pinned install is refused, the unpinned **retry is a second full package-manager run
|
|
98
|
+
Two things that trace makes visible and nothing else did. Planning is ~2ms, so init's wall-clock is essentially all subprocess: the package manager, and `claude mcp add` when MCP registration is on. And when the pinned install is refused, the unpinned **retry is a second full package-manager run**, so init legitimately takes about twice as long on that path. Every `--local` fixture run takes it, because the version being installed is not published yet.
|
|
93
99
|
|
|
94
100
|
### Adding a span
|
|
95
101
|
|
|
@@ -99,4 +105,4 @@ import { span } from '../trace.js';
|
|
|
99
105
|
const result = await span('predicate.eval', { kind: predicate.kind }, () => evaluate(predicate));
|
|
100
106
|
```
|
|
101
107
|
|
|
102
|
-
Wrap a stage worth a line in a bottleneck hunt
|
|
108
|
+
Wrap a stage worth a line in a bottleneck hunt: a session resolve, a browser round-trip, a predicate evaluation. Not every function: the trace earns its keep by being faster to read than the code, and that stops being true somewhere around one line per statement. Nesting needs no plumbing: the call id and depth ride in `AsyncLocalStorage`, so a span added five frames down still lands in the right tree without changing anyone's signature.
|
package/docs/deploy-checks.md
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Deploy checks
|
|
3
|
+
description: 'Enforce verification at the moment code becomes a preview URL, as a Vercel or Netlify check on the PR.'
|
|
4
|
+
icon: circle-check
|
|
5
|
+
---
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
To gate a PR on Reticle, run `npx @reticlehq/server verify <preview-url>` in CI when the preview deploy succeeds. It exits **0** when every saved flow passes and **1** otherwise, so the job's own pass/fail becomes the check on the PR. There is no workflow-level integration to write beyond that one command.
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
The strongest place to enforce verification is the moment code becomes a preview URL. Every deploy already produces one, and both Vercel and Netlify let a third party attach a **check** to it: pass/fail, shown on the PR, with no workflow file to write.
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
That makes the non-developer story complete: the SDK is auto-injected by the build plugin, flows are minted from toolbar recordings with auto-proposed consequences, and verification runs at publish, **without anyone writing a test.**
|
|
12
|
+
|
|
13
|
+
> **Recipe A works today; Recipe B does not exist yet.** `reticle verify` exits 0/1 and persists a run artifact right now. What is _not_ built is a hosted Reticle app that registers itself as a Vercel or Netlify check provider. Until it exists, use the CI recipe.
|
|
8
14
|
|
|
9
15
|
## The shape
|
|
10
16
|
|
|
@@ -14,11 +20,11 @@ git push → preview deploy → Reticle verifies the preview URL → check passe
|
|
|
14
20
|
|
|
15
21
|
`reticle verify <preview-url>` is the whole integration surface:
|
|
16
22
|
|
|
17
|
-
- exits **0** when every saved flow passes, **1** otherwise
|
|
23
|
+
- exits **0** when every saved flow passes, **1** otherwise (the only contract a check needs),
|
|
18
24
|
- prints a legible ✓/✗ report for the PR log,
|
|
19
25
|
- persists a `ReticleVerificationRun` artifact (`.reticle/runs/<id>.json`) so `reticle gate` and `reticle_run_export` can consume the same verdict.
|
|
20
26
|
|
|
21
|
-
## Recipe A
|
|
27
|
+
## Recipe A: CI (works today, any provider)
|
|
22
28
|
|
|
23
29
|
```yaml
|
|
24
30
|
# .github/workflows/verify.yml
|
|
@@ -31,17 +37,22 @@ jobs:
|
|
|
31
37
|
steps:
|
|
32
38
|
- uses: actions/checkout@v4
|
|
33
39
|
- uses: pnpm/action-setup@v4
|
|
40
|
+
# setup-node must come AFTER pnpm/action-setup, or its pnpm cache has no pnpm to find.
|
|
41
|
+
- uses: actions/setup-node@v4
|
|
42
|
+
with:
|
|
43
|
+
node-version: 22
|
|
44
|
+
cache: pnpm
|
|
34
45
|
- run: pnpm install --frozen-lockfile
|
|
35
46
|
- run: npx playwright install --with-deps chromium
|
|
36
47
|
# Reticle drives the preview and owns its own browser + daemon.
|
|
37
|
-
- run: npx
|
|
48
|
+
- run: npx @reticlehq/server verify "${{ github.event.deployment_status.target_url }}" --timeout 60000
|
|
38
49
|
```
|
|
39
50
|
|
|
40
51
|
The job's own pass/fail becomes the PR check. Nothing else is required.
|
|
41
52
|
|
|
42
|
-
**Non-loopback previews need pairing.** For a real preview URL (not `localhost`), Reticle injects `reticle.connect()` with a one-time token and allow-lists the preview origin
|
|
53
|
+
**Non-loopback previews need pairing.** For a real preview URL (not `localhost`), Reticle injects `reticle.connect()` with a one-time token and allow-lists the preview origin, so the app does not need to be rebuilt per environment. Confirm the SDK actually runs on the deployed build: a production build that tree-shakes the dev-only SDK will connect to nothing, and `verify` will (correctly) fail with _"no app connected."_
|
|
43
54
|
|
|
44
|
-
## Recipe B
|
|
55
|
+
## Recipe B: the Checks API pattern (what a hosted Reticle would do)
|
|
45
56
|
|
|
46
57
|
Both providers expose the same shape, which is why this is one integration rather than two:
|
|
47
58
|
|
|
@@ -54,10 +65,10 @@ The flow is: receive the deploy webhook → `reticle verify <target_url>` → po
|
|
|
54
65
|
|
|
55
66
|
## Pair it with the local gate
|
|
56
67
|
|
|
57
|
-
The deploy check catches what reaches a preview. `reticle gate` catches it earlier
|
|
68
|
+
The deploy check catches what reaches a preview. `reticle gate` catches it earlier: an agent that edits a covered file cannot "finish" without re-verifying:
|
|
58
69
|
|
|
59
70
|
```bash
|
|
60
|
-
|
|
71
|
+
npx @reticlehq/server gate --since origin/main # exit 1 unless passing artifacts cover the affected flows
|
|
61
72
|
```
|
|
62
73
|
|
|
63
74
|
Use both: `gate` in the agent's Stop hook (see `agent-cheatsheet`), `verify` at the deploy. They read the same run artifacts, so a green gate locally and a green check on the PR mean the same thing.
|
|
@@ -66,4 +77,26 @@ Use both: `gate` in the agent's Stop hook (see `agent-cheatsheet`), `verify` at
|
|
|
66
77
|
|
|
67
78
|
- **`verify` needs one connected session.** If several tabs of the app are open against the same daemon it refuses rather than guessing which to drive.
|
|
68
79
|
- **It replays flows sequentially against one tab**, so flows must not depend on each other's leftover state (a flow that logs in contaminates the next one). Author self-contained flows, or use `reticle_flow_verify { parallel }`, which gives each flow an isolated context.
|
|
69
|
-
- **No saved flows means nothing to verify
|
|
80
|
+
- **No saved flows means nothing to verify.** `verify` fails rather than reporting a vacuous pass.
|
|
81
|
+
|
|
82
|
+
## FAQ
|
|
83
|
+
|
|
84
|
+
### verify says 'no app connected' on my preview URL. Why?
|
|
85
|
+
|
|
86
|
+
Almost always because the deployed build tree-shook the dev-only SDK out, so there is nothing in the page dialling the bridge. `reticle.connect()` is meant to be dev-gated, and a production build honours that gate. Either build the preview with the dev guard satisfied, or accept that `verify` is correctly reporting that it cannot see the app. It is not a false alarm: with no SDK there is no program to read.
|
|
87
|
+
|
|
88
|
+
### Does it work on a real preview URL, or only localhost?
|
|
89
|
+
|
|
90
|
+
Both. For a non-loopback origin Reticle injects `reticle.connect()` with a one-time pairing token and allow-lists the preview origin, so you do not rebuild per environment. The bridge refuses to bind a non-loopback host without a token, which is the point.
|
|
91
|
+
|
|
92
|
+
### One of my flows logs in and the next one starts logged in. How do I stop that?
|
|
93
|
+
|
|
94
|
+
`verify` replays flows sequentially against one tab, so leftover state carries. Either author each flow to be self-contained, or use `reticle_flow_verify { parallel: true }`, which gives every flow its own isolated browser context with its own cookies and storage.
|
|
95
|
+
|
|
96
|
+
### Should I run reticle gate as well as reticle verify?
|
|
97
|
+
|
|
98
|
+
If you want an agent to be unable to call a change finished without re-verifying, yes. They catch the same regression at different moments: `gate` runs locally against the files the agent just edited, `verify` runs against the deployed preview. They read the same run artifacts, so a green gate locally and a green check on the PR mean the same thing.
|
|
99
|
+
|
|
100
|
+
### verify exited 1 but I cannot see which flow failed.
|
|
101
|
+
|
|
102
|
+
The verdict is in the run artifact, not only in the log. Each run writes `.reticle/runs/<id>.json` with `repair.failurePackets[]` naming what broke and where; `reticle_run_export { format: "report" }` renders it legibly. The artifact is stable and versioned precisely so a CI surface can render it without parsing log lines.
|
package/docs/desktop.mdx
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Desktop apps: no browser required'
|
|
3
|
+
sidebarTitle: Desktop apps
|
|
4
|
+
description: 'Verify Electron and Tauri apps from the inside with Reticle, including the IPC boundary the renderer cannot see. Setup, capabilities, screenshots, headless mode, and troubleshooting for both runtimes.'
|
|
5
|
+
icon: display
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Reticle verifies Electron and Tauri apps the same way it verifies web apps: from **inside** the app, over a localhost WebSocket. There is no URL to open, no browser to launch, and no screenshot to interpret. Your app dials the daemon, and the agent drives it with the same tools it uses on the web.
|
|
9
|
+
|
|
10
|
+
This page is the complete desktop guide. Everything you need to wire, drive, and debug both runtimes is here.
|
|
11
|
+
|
|
12
|
+
## How you actually test a desktop app
|
|
13
|
+
|
|
14
|
+
The usual question is "it's a desktop app, so what URL does the agent open?". There isn't one, and that is the whole problem. The direction is reversed from what browser tooling trains you to expect:
|
|
15
|
+
|
|
16
|
+
```mermaid
|
|
17
|
+
flowchart LR
|
|
18
|
+
A["coding agent"]
|
|
19
|
+
D["reticle daemon<br/>localhost:4400"]
|
|
20
|
+
W["your Electron / Tauri<br/>window + the SDK"]
|
|
21
|
+
|
|
22
|
+
A <-- "MCP over stdio" --> D
|
|
23
|
+
W -- "the app dials OUT<br/>over WebSocket" --> D
|
|
24
|
+
D -- "commands" --> W
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Your app **connects to the daemon**, not the other way round. So the workflow is:
|
|
28
|
+
|
|
29
|
+
1. **Start the daemon once.**
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx @reticlehq/server serve
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
2. **Start your app exactly as you always do.**
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm run dev # or: electron . | cargo tauri dev
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
3. **Confirm it connected.**
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx @reticlehq/server status
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Your window appears as a session, and the agent can drive it.
|
|
48
|
+
|
|
49
|
+
`reticle open` has nothing to open for a desktop app and will say so, and there is no `reticle drive` for desktop: those launch a browser, which is not what you are testing. Headless works on both runtimes, covered below.
|
|
50
|
+
|
|
51
|
+
## The thing nobody else does
|
|
52
|
+
|
|
53
|
+
A desktop app's most interesting failures happen at the **IPC boundary**. The renderer asks the main process for something, and the main process quietly fails.
|
|
54
|
+
|
|
55
|
+
The renderer cannot see that. A screenshot certainly cannot. Reticle records it as a request:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{ "method": "ipc", "url": "ipc://todos:archive", "status": 500, "ms": 82 }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Then it contradicts a UI that moved on regardless:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"kind": "signal-contradicted",
|
|
66
|
+
"claim": "the app fired \"todos:loaded\"",
|
|
67
|
+
"counter": "1 request(s) in the same window failed",
|
|
68
|
+
"detail": "IPC ipc://todos:archive → 500"
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The app said it loaded. One IPC call in the same window returned 500. Reticle puts both in front of the agent instead of believing the happier one.
|
|
73
|
+
|
|
74
|
+

|
|
75
|
+
|
|
76
|
+
<Note>
|
|
77
|
+
The window on the left is an illustration. The JSON on the right is real output from the desktop
|
|
78
|
+
battery, which starts a real Electron main process and a **packaged** Tauri binary and drives them
|
|
79
|
+
headless on every change.
|
|
80
|
+
</Note>
|
|
81
|
+
|
|
82
|
+
## What works, measured
|
|
83
|
+
|
|
84
|
+
Every tool below was run against both demo apps against a live daemon.
|
|
85
|
+
|
|
86
|
+
The rows in **bold** are re-proven by a committed battery on every change, `pnpm test:e2e:desktop`, which starts a real Electron main process and a **packaged** Tauri binary (`tauri://localhost`, not `tauri dev`) and drives them headless. The rest were measured by hand. That distinction matters: this table used to report a hand-run score with nothing in the repo that reproduced it, so it could go stale without anything failing.
|
|
87
|
+
|
|
88
|
+
| Capability | Electron | Tauri | Note |
|
|
89
|
+
| --- | --- | --- | --- |
|
|
90
|
+
| sessions, snapshot, query, inspect | yes | yes | `inspect` returns `src/App.tsx:104` in a dev build; a packaged production renderer has no source map, so it reports `n/a` |
|
|
91
|
+
| capabilities, state (live store) | yes | yes | `reticle_state` reads the real store |
|
|
92
|
+
| act (click/fill/type/select) | yes | yes | |
|
|
93
|
+
| **act_and_wait, wait_for, assert** | yes | yes | signal / state / route / net / console predicates |
|
|
94
|
+
| console errors | yes | yes | catches what the UI never shows |
|
|
95
|
+
| network (HTTP) | yes | yes | on `file://` a relative URL has no origin; use an absolute one |
|
|
96
|
+
| **network (IPC)** | yes | yes | `ipc://<channel>`, including failures. Electron: `invoke` and `sendSync` carry a verdict; a one-way `send` is recorded as `oneWay: true` with NO status, because the renderer never learns the outcome. Tauri: both the `ipc://` (macOS/Linux) and `http://ipc.localhost` (Windows) transports |
|
|
97
|
+
| route | yes | yes | use a **hash** router, see below |
|
|
98
|
+
| storage, animations, observe, explore | yes | yes | |
|
|
99
|
+
| baseline (semantic), record then replay, crawl | yes | yes | `crawl` found real anomalies in both |
|
|
100
|
+
| navigate (reload) | yes | yes | |
|
|
101
|
+
| **screenshot / visual_diff** | yes | yes | one line in Electron's main process, one Rust command in Tauri, both below. Reticle's own presenter panel is hidden for the shot, so a baseline records your app and not the instrument |
|
|
102
|
+
| **drivable while window occluded** | yes | yes | including minimized, app-hidden, and behind a fullscreen app on another Space |
|
|
103
|
+
| **headless** | yes | yes | Electron never shows the window; Tauri shows, loads, then hides |
|
|
104
|
+
| **a missing preload is DECLARED, not silent** | yes | n/a | without `@reticlehq/electron/preload` every IPC call is invisible, so verdicts carry `coverage: partial` naming the missing line instead of reading clean |
|
|
105
|
+
| network_mock, viewport | no | no | need a Reticle-driven browser |
|
|
106
|
+
|
|
107
|
+
## Three things that are genuinely different
|
|
108
|
+
|
|
109
|
+
<CardGroup cols={3}>
|
|
110
|
+
<Card title="It works occluded" icon="eye-slash">
|
|
111
|
+
Minimized, app-hidden, behind a fullscreen app on another Space. The agent keeps driving.
|
|
112
|
+
</Card>
|
|
113
|
+
<Card title="Screenshots cannot lie" icon="camera">
|
|
114
|
+
Captured from the window's own backing store, never the screen.
|
|
115
|
+
</Card>
|
|
116
|
+
<Card title="Missing wiring is declared" icon="triangle-exclamation">
|
|
117
|
+
Electron without the preload reports `coverage: partial`, not a clean pass.
|
|
118
|
+
</Card>
|
|
119
|
+
</CardGroup>
|
|
120
|
+
|
|
121
|
+
## Electron setup
|
|
122
|
+
|
|
123
|
+
Two steps. The first is the ordinary web setup; the second is the only desktop-specific part.
|
|
124
|
+
|
|
125
|
+
**1. The renderer.** One line in `vite.config.ts`, exactly like a web app:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { reticle } from '@reticlehq/vite-plugin';
|
|
129
|
+
|
|
130
|
+
export default defineConfig({
|
|
131
|
+
base: './', // file:// needs relative asset paths
|
|
132
|
+
plugins: [react(), reticle({ desktop: true })],
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`desktop: true` does the two things a desktop shell needs and a web app must never get: the plugin also runs for `vite build` (a packaged renderer is a production build with **no dev server**, so the default serve-only gating would ship an app with no `connect()` at all), and `connect()` is called with `allowInProduction` so the SDK's production backstop does not refuse to start. Keep it behind your own dev-only build so an instrumented bundle can never reach a release binary.
|
|
137
|
+
|
|
138
|
+
Nothing to add in your app code. You can still call `reticle.connect()` by hand and pass `inject: false` if you want control.
|
|
139
|
+
|
|
140
|
+
**2. The preload.** One line, before you expose anything:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npm i -D @reticlehq/electron
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
// electron/preload.cjs
|
|
148
|
+
require('@reticlehq/electron/preload');
|
|
149
|
+
|
|
150
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
151
|
+
contextBridge.exposeInMainWorld('api', {
|
|
152
|
+
loadTodos: () => ipcRenderer.invoke('todos:load'),
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
That line is what makes your main-process calls visible. It has to live in the preload, and it is not a stylistic choice: `contextBridge.exposeInMainWorld` hands the renderer a **deeply frozen, non-configurable** object, so nothing running in the page can instrument `window.api`. The preload is the last point where `ipcRenderer.invoke` is still an ordinary, writable function. Patching there covers every channel you go on to expose, whatever you named it.
|
|
157
|
+
|
|
158
|
+
**Preload sandboxing.** A sandboxed preload cannot resolve `node_modules`, so the bare `require` above fails with `module not found: @reticlehq/electron/preload`. Either bundle your preload (electron-vite and Electron Forge do this by default; the require is inlined at build time and sandboxing stays on), or set `sandbox: false` in `webPreferences`.
|
|
159
|
+
|
|
160
|
+
**Packaged renderers.** An app that loads its renderer with `loadFile` runs on `file://`, which is a production Vite build. Pass `allowInProduction: true` to `connect()` for that mode, or keep the SDK gated behind `import.meta.env.DEV` so it never enters the shipped binary at all.
|
|
161
|
+
|
|
162
|
+
Working example: [`apps/electron-smoke`](https://github.com/reticlehq/reticle/tree/main/apps/electron-smoke).
|
|
163
|
+
|
|
164
|
+
## Tauri setup
|
|
165
|
+
|
|
166
|
+
Frontend side, nothing desktop-specific:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
// src/main.tsx
|
|
170
|
+
import { reticle } from '@reticlehq/browser';
|
|
171
|
+
|
|
172
|
+
if (import.meta.env.DEV) reticle.connect();
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Nothing else is needed for IPC. A Tauri `invoke` travels as a real `fetch` to Tauri's `ipc://` custom protocol, so Reticle already sees it; every `invoke('load_todos')` shows up as `ipc://load_todos`. Reticle also reads Tauri's `Tauri-Response` header, because the transport answers **HTTP 200 whether the Rust command returned `Ok` or `Err`**. Without that translation a failed command would be recorded as a successful request.
|
|
176
|
+
|
|
177
|
+
The one required step is **CSP**. Tauri ships a restrictive default that blocks the bridge WebSocket before it opens, and the failure is silent from the app's side. In `src-tauri/tauri.conf.json`:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"app": {
|
|
182
|
+
"security": {
|
|
183
|
+
"csp": "default-src 'self' ipc: http://ipc.localhost; connect-src 'self' ipc: http://ipc.localhost ws://localhost:4400 ws://127.0.0.1:4400"
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
<Warning>
|
|
190
|
+
Tauri's default CSP blocks the bridge WebSocket **before it opens**. Your app runs perfectly and
|
|
191
|
+
simply never connects, with no error to go on. Keep `ipc: http://ipc.localhost` in `connect-src`,
|
|
192
|
+
because Tauri v2 needs it for `invoke` itself. Add your dev-server origin too if you use `devUrl`.
|
|
193
|
+
This is a dev-only config, so drop the `ws://` entries from your release config.
|
|
194
|
+
</Warning>
|
|
195
|
+
|
|
196
|
+
Working example: [`apps/tauri-smoke`](https://github.com/reticlehq/reticle/tree/main/apps/tauri-smoke).
|
|
197
|
+
|
|
198
|
+
## Screenshots
|
|
199
|
+
|
|
200
|
+
**Electron: one line in the main process.**
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
const { installReticleCapture } = require('@reticlehq/electron/main');
|
|
204
|
+
|
|
205
|
+
const win = new BrowserWindow({/* ... */});
|
|
206
|
+
installReticleCapture(win);
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
That is all: no CDP flag, no extra packages, and it works on a packaged `file://` renderer. `reticle_screenshot` and `reticle_visual_diff` then work on your app.
|
|
210
|
+
|
|
211
|
+
Alternatively, since an Electron renderer _is_ Chromium, `--remote-debugging-port=9222` plus `RETICLE_CDP_URL=http://127.0.0.1:9222` also works and additionally enables `fullPage` (the main-process route captures the window as composited, so it cannot scroll-stitch).
|
|
212
|
+
|
|
213
|
+
**Tauri: one Rust command.** The crate is versioned independently of the npm packages:
|
|
214
|
+
|
|
215
|
+
```toml
|
|
216
|
+
[dependencies]
|
|
217
|
+
reticle-tauri = "0.1"
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
```rust
|
|
221
|
+
tauri::Builder::default()
|
|
222
|
+
.invoke_handler(tauri::generate_handler![reticle_tauri::reticle_capture])
|
|
223
|
+
.on_page_load(reticle_tauri::on_page_load)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Nothing on the JavaScript side: the SDK invokes the command through Tauri's own internals, because Tauri has no preload stage where a shim could be installed.
|
|
227
|
+
|
|
228
|
+
### Why the backing store, and not a screen capture
|
|
229
|
+
|
|
230
|
+
`webContents.capturePage()` reads the window's own backing store. Capturing a screen _region_ instead was tried and deliberately rejected: it photographs whatever is on top, so an app window behind your editor yields a picture of the editor, saved as a visual baseline that a later diff would trust. A screenshot tool that can silently return another window's pixels manufactures exactly the false green Reticle exists to eliminate.
|
|
231
|
+
|
|
232
|
+
`reticle_capture` on Tauri renders the webview for the same reason, so it needs no screen-recording permission, cannot return another window's pixels, and is correct with nothing on screen at all. Each platform uses its own webview API:
|
|
233
|
+
|
|
234
|
+
| Platform | API | Status |
|
|
235
|
+
| --- | --- | --- |
|
|
236
|
+
| macOS | `WKWebView.takeSnapshot` | Verified against a running app |
|
|
237
|
+
| Linux, BSD | WebKitGTK `webkit_web_view_get_snapshot` | Snapshot plus PNG encoding verified under `xvfb`, not yet driven through a full Tauri app |
|
|
238
|
+
| Windows | WebView2 `CapturePreview` | **Untested: compiles, never executed** |
|
|
239
|
+
|
|
240
|
+
The Windows path is written and type-checked against the real `webview2-com` API (which caught two genuine type errors), but nobody has run it on Windows. CI re-checks it against that target on every PR (`cargo check --target x86_64-pc-windows-msvc`), so "compiles" is a gate rather than a claim. Executed is still a different word from compiled: it is shipped rather than withheld so it can be tried, and labelled rather than listed flatly so that trying it is a choice. If it works for you, say so and this row changes; treat a green from it as unconfirmed until then.
|
|
241
|
+
|
|
242
|
+
All three capture the visible viewport by default, so a baseline taken on a developer's Mac is comparable against the same app in Linux CI. On a platform with no webview API to call, capture reports no-provider rather than returning a plausible wrong image.
|
|
243
|
+
|
|
244
|
+
**`{ fullPage: true }` works on Tauri/Linux only.** WebKitGTK can render the whole document offscreen; `takeSnapshot` (macOS) and `CapturePreview` (Windows) only give what is composited, and Electron's `capturePage()` is the same. Asked for a full page they cannot produce, all of them return `{ ok: false, reason: 'full-page-unsupported' }` rather than quietly handing back the viewport, which would be a baseline that omits everything below the fold while every later diff of it reports green about a region that was never captured. No baseline is written on a refusal.
|
|
245
|
+
|
|
246
|
+
An app that already has its own capture can expose `window.__reticleIpc.capture()` returning a PNG path instead; the SDK prefers it over the built-in command.
|
|
247
|
+
|
|
248
|
+
<Warning>
|
|
249
|
+
One honest caveat. A fully occluded or minimized window is only partially composited, so parts of
|
|
250
|
+
a capture may come back blank. Bring the window forward for a complete image. It is never the
|
|
251
|
+
wrong window.
|
|
252
|
+
</Warning>
|
|
253
|
+
|
|
254
|
+
## Headless
|
|
255
|
+
|
|
256
|
+
**Electron: yes.** `show: false` plus `backgroundThrottling: false` in `webPreferences`. The second one is load-bearing: Chromium runs an unshown window's timers in slow motion, which turns every settle wait into a flake. Screenshots still work, because `capturePage` reads the backing store rather than the screen. Verified with a full tool drive against a window that was never shown.
|
|
257
|
+
|
|
258
|
+
**Tauri: yes. Show, load, then hide.**
|
|
259
|
+
|
|
260
|
+
```rust
|
|
261
|
+
.on_page_load(reticle_tauri::on_page_load) // hides the window when RETICLE_HEADLESS=1
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Run with `RETICLE_HEADLESS=1 pnpm tauri dev`. Nothing ends up on screen, and screenshots keep working because the capture renders the webview rather than the screen.
|
|
265
|
+
|
|
266
|
+
The ordering is the whole trick. Hiding the window during `setup` hides it before the webview has ever been presented, and a webview that has never been presented never loads its page, which is what made headless Tauri look impossible. Hiding it after its first page load leaves everything running. Verified with a full 43-tool drive plus a screenshot and a visual diff against a window that is not on screen.
|
|
267
|
+
|
|
268
|
+
`xvfb-run -a pnpm tauri dev` also works on Linux and needs no app-side change at all.
|
|
269
|
+
|
|
270
|
+
### A correction worth recording: the Tauri macOS "liveness constraint" was wrong
|
|
271
|
+
|
|
272
|
+
Earlier versions of this document said a Tauri app on macOS is only drivable while its window is on the active Space and unoccluded, and that hiding it suspends the webview. **That is not true, and the mistake is worth recording because it cost three features.**
|
|
273
|
+
|
|
274
|
+
Re-measured against the live app, a loaded Tauri webview answers Reticle commands at full speed while minimized, app-hidden with Cmd-H, fully occluded, on another Space behind a fullscreen app, and with no window on screen at all. A full 43-tool drive passes in every one of those states.
|
|
275
|
+
|
|
276
|
+
What actually failed was narrower: **a webview that has never been presented never loads its page.** Every "suspension" experiment hid or moved the window from `setup`, meaning before the first present, so the page never ran and every command timed out at 8s. The timeouts were real; the diagnosis was not. The `alwaysOnTop` workaround was then built to fix a problem that did not exist, measured as "still broken" for the same reason, and deleted.
|
|
277
|
+
|
|
278
|
+
The lesson generalises past this document: four experiments agreeing does not make a conclusion controlled, if all four share the same confound.
|
|
279
|
+
|
|
280
|
+
## Routing: use a hash router
|
|
281
|
+
|
|
282
|
+
A packaged renderer runs on `file://`, where `pushState('/settings')` rewrites the URL to `file:///settings`, a path that does not exist, so the next reload lands on a blank page and the app is gone. This is why HashRouter is the standard choice for packaged Electron and Tauri apps. Reticle's route observer handles both, and a `{ kind: 'route', contains: ... }` assertion matches the fragment.
|
|
283
|
+
|
|
284
|
+
## What IPC looks like to an agent
|
|
285
|
+
|
|
286
|
+
A desktop app reaches its backend over IPC, not HTTP. `fetch`/`XHR` patching cannot see that, so without the IPC observer every backend call in your app is a blind spot: `reticle_network` returns nothing, `act_and_wait` has no in-flight request to settle on, and `assert { net }` is vacuously true. That is a false green by construction.
|
|
287
|
+
|
|
288
|
+
Reticle records each IPC call as an ordinary request, so the tools you already use work unchanged:
|
|
289
|
+
|
|
290
|
+
```jsonc
|
|
291
|
+
// reticle_network { urlContains: "ipc://" }
|
|
292
|
+
{
|
|
293
|
+
"calls": [
|
|
294
|
+
{ "method": "ipc", "url": "ipc://todos:load", "status": 200, "ms": 134 },
|
|
295
|
+
{ "method": "ipc", "url": "ipc://todos:archive", "status": 500, "ms": 83 },
|
|
296
|
+
],
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
IPC has no status code. `200`/`500` are synthetic, mapped from whether the call succeeded or failed, precisely so that `reticle_network { status: 500 }` and `assert { kind: "net", status: 500 }` keep working. On Tauri you will see `status: 500` next to `statusText: "OK"`. That is not a bug: the transport really did answer 200, and the 500 is the command's own verdict. `ok` is authoritative, and on Electron `error` carries the message your main process returned:
|
|
301
|
+
|
|
302
|
+
```jsonc
|
|
303
|
+
// reticle_assert { predicate: { kind: "net", urlContains: "ipc://todos:archive", status: 500 } }
|
|
304
|
+
{
|
|
305
|
+
"pass": true,
|
|
306
|
+
"evidence": {
|
|
307
|
+
"url": "ipc://todos:archive",
|
|
308
|
+
"ok": false,
|
|
309
|
+
"status": 500,
|
|
310
|
+
"error": "archive is not implemented in the backend",
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Both example apps ship a planted false green: an Archive button that updates the UI optimistically and swallows the rejection. The screen says "archived", a screenshot agrees, a DOM assertion agrees. Only the IPC record disagrees. That is the case desktop support exists for.
|
|
316
|
+
|
|
317
|
+
### Why a missing preload is not a silent pass
|
|
318
|
+
|
|
319
|
+
Without `@reticlehq/electron/preload`, every IPC call is invisible to Reticle. A tool that just reported clean there would be telling you the IPC layer is fine when it never looked. Instead verdicts carry `coverage: partial` and name the missing line.
|
|
320
|
+
|
|
321
|
+
## How it compares to Playwright MCP
|
|
322
|
+
|
|
323
|
+
Both attached to the same running Electron app, same task ("archive a todo, then verify it worked"):
|
|
324
|
+
|
|
325
|
+
| tool | ~tokens | ms | verdict |
|
|
326
|
+
| ----------------------- | ------- | ------- | ----------------------------------------- |
|
|
327
|
+
| reticle (lean) | **350** | 1364 | caught the failure |
|
|
328
|
+
| playwright-mcp | 1069 | **980** | blind to it: no network/IPC in its output |
|
|
329
|
+
| playwright-mcp to Tauri | n/a | n/a | cannot attach (no CDP in WKWebView) |
|
|
330
|
+
|
|
331
|
+
Playwright MCP is faster. It is also structurally unable to see an IPC failure, because its channel is the accessibility tree.
|
|
332
|
+
|
|
333
|
+
## Troubleshooting
|
|
334
|
+
|
|
335
|
+
**`reticle status` shows no session.** Check the app's console. On Electron open devtools, or forward `console-message` to your terminal, since a desktop renderer has no visible console otherwise. A refused connect always logs why.
|
|
336
|
+
|
|
337
|
+
**Tauri: nothing connects and the app console shows a CSP violation.** The `connect-src` above is missing, or does not include your daemon's port. See [Tauri setup](#tauri-setup).
|
|
338
|
+
|
|
339
|
+
**Electron: `module not found: @reticlehq/electron/preload`.** The preload is sandboxed. Bundle it, or set `sandbox: false`. See [Electron setup](#electron-setup).
|
|
340
|
+
|
|
341
|
+
**IPC calls do not appear, but the app works.** On Electron the shim's `require` must run _before_ your preload captures its own reference to `ipcRenderer`, so put it on the first line. On Tauri, `invoke` imported from `@tauri-apps/api/core` is observed; a hand-rolled `postMessage` protocol is not, and neither is Tauri's `postMessage` transport fallback on platforms where the `ipc://` custom protocol is unavailable.
|
|
342
|
+
|
|
343
|
+
**`reticle open` says there is nothing to open.** That is correct for a desktop app. There is no URL, and there is no `reticle drive` for desktop either. Both launch a browser, which is not what you are testing.
|
|
344
|
+
|
|
345
|
+
**A screenshot came back partly blank.** The window is occluded or minimized, so it is only partially composited. Bring it forward and capture again.
|
|
346
|
+
|
|
347
|
+
**`{ ok: false, reason: 'full-page-unsupported' }`.** You asked for `fullPage: true` on a platform that cannot render offscreen. Only Tauri on Linux (WebKitGTK) can. Capture the viewport instead, or scroll and capture in sections.
|
|
348
|
+
|
|
349
|
+
## FAQ
|
|
350
|
+
|
|
351
|
+
<AccordionGroup>
|
|
352
|
+
<Accordion title="Do I need a browser installed to verify a desktop app?">
|
|
353
|
+
No. Reticle never launches Chromium for a desktop app. Your app connects to the local daemon,
|
|
354
|
+
and the agent drives that connection.
|
|
355
|
+
</Accordion>
|
|
356
|
+
<Accordion title="Does my app have to be visible on screen?">
|
|
357
|
+
No. A loaded window is drivable while minimized, app-hidden, occluded, or on another Space. The
|
|
358
|
+
one requirement is that the window was presented at least once, because a webview that has never
|
|
359
|
+
been presented never loads its page.
|
|
360
|
+
</Accordion>
|
|
361
|
+
<Accordion title="Can Playwright do this instead?">
|
|
362
|
+
For Electron, partly: it attaches over CDP and sees the accessibility tree, but not IPC. For
|
|
363
|
+
Tauri, no: WKWebView exposes no CDP endpoint, so Playwright cannot attach at all.
|
|
364
|
+
</Accordion>
|
|
365
|
+
<Accordion title="Does the SDK end up in my shipped binary?">
|
|
366
|
+
Only if you put it there. Keep the plugin behind a dev-only build, or gate `connect()` on
|
|
367
|
+
`import.meta.env.DEV`. `desktop: true` exists because a packaged renderer is a production build,
|
|
368
|
+
so it opts that one build into instrumentation deliberately.
|
|
369
|
+
</Accordion>
|
|
370
|
+
<Accordion title="Why does a Tauri response show status 500 with statusText OK?">
|
|
371
|
+
The IPC transport really did answer HTTP 200. The 500 is Reticle translating the Rust command's
|
|
372
|
+
own `Err` result so that `status: 500` filters and assertions keep working. `ok` is
|
|
373
|
+
authoritative.
|
|
374
|
+
</Accordion>
|
|
375
|
+
</AccordionGroup>
|
|
376
|
+
|
|
377
|
+
<CardGroup cols={2}>
|
|
378
|
+
<Card title="Electron package" icon="cube" href="/packages/electron">
|
|
379
|
+
`@reticlehq/electron`: the preload shim and the main-process capture.
|
|
380
|
+
</Card>
|
|
381
|
+
<Card title="Tauri package" icon="cube" href="/packages/tauri">
|
|
382
|
+
`reticle-tauri`: the Rust crate for capture and headless mode.
|
|
383
|
+
</Card>
|
|
384
|
+
</CardGroup>
|