@reticlehq/server 2.8.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (318) hide show
  1. package/SKILL.md +130 -813
  2. package/dist/bridge/bridge.d.ts +23 -0
  3. package/dist/bridge/bridge.js +80 -2
  4. package/dist/bridge/bridge.test-harness.d.ts +5 -0
  5. package/dist/bridge/bridge.test-harness.js +6 -4
  6. package/dist/capsule/capsule-store.d.ts +2 -2
  7. package/dist/capsule/causal-summary.d.ts +16 -1
  8. package/dist/capsule/causal-summary.js +2 -1
  9. package/dist/cli/cli-doctor.js +41 -13
  10. package/dist/cli/cli-flow-commands.js +8 -8
  11. package/dist/cli/cli-launch.d.ts +0 -17
  12. package/dist/cli/cli-launch.js +0 -28
  13. package/dist/cli/cli-parse.d.ts +1 -1
  14. package/dist/cli/cli-parse.js +1 -1
  15. package/dist/cli/cli-port.d.ts +24 -0
  16. package/dist/cli/cli-port.js +66 -35
  17. package/dist/cli/cli-verify.d.ts +21 -0
  18. package/dist/cli/cli-verify.js +45 -1
  19. package/dist/cli/config-discovery.d.ts +35 -0
  20. package/dist/cli/config-discovery.js +163 -0
  21. package/dist/cli/doctor-daemon-line.js +4 -1
  22. package/dist/cli/doctor-project-line.d.ts +37 -0
  23. package/dist/cli/doctor-project-line.js +35 -0
  24. package/dist/cli/doctor-rows.d.ts +51 -0
  25. package/dist/cli/doctor-rows.js +54 -0
  26. package/dist/cli/doctor-sessions-line.js +6 -3
  27. package/dist/cli/drive-attach.d.ts +99 -0
  28. package/dist/cli/drive-attach.js +203 -0
  29. package/dist/cli/drive-command.d.ts +13 -0
  30. package/dist/cli/drive-command.js +103 -0
  31. package/dist/cli/open-note.d.ts +20 -0
  32. package/dist/cli/open-note.js +37 -0
  33. package/dist/cli/status-next-action.d.ts +37 -0
  34. package/dist/cli/status-next-action.js +61 -0
  35. package/dist/cli.js +65 -70
  36. package/dist/crawl/crawl.d.ts +7 -2
  37. package/dist/crawl/crawl.js +52 -9
  38. package/dist/ee/audit-log.js +2 -2
  39. package/dist/events/body-failures.d.ts +2 -2
  40. package/dist/events/contradiction-folds.d.ts +45 -0
  41. package/dist/events/contradiction-folds.js +42 -0
  42. package/dist/events/contradictions.d.ts +51 -1
  43. package/dist/events/contradictions.js +26 -7
  44. package/dist/events/declared.d.ts +45 -0
  45. package/dist/events/declared.js +73 -0
  46. package/dist/events/echo-mismatch.d.ts +9 -3
  47. package/dist/events/echo-mismatch.js +33 -5
  48. package/dist/events/event-filters.d.ts +1 -1
  49. package/dist/events/event-filters.js +7 -4
  50. package/dist/events/predicate-eval.d.ts +17 -91
  51. package/dist/events/predicate-eval.js +85 -326
  52. package/dist/events/predicate-parse.js +31 -3
  53. package/dist/events/predicate-route.d.ts +32 -0
  54. package/dist/events/predicate-route.js +102 -0
  55. package/dist/events/predicate-schema.d.ts +99 -0
  56. package/dist/events/predicate-schema.js +309 -0
  57. package/dist/events/predicate.d.ts +8 -0
  58. package/dist/events/predicate.js +127 -5
  59. package/dist/events/stale-response.d.ts +2 -3
  60. package/dist/events/stale-response.js +49 -2
  61. package/dist/events/unit-mismatch.d.ts +2 -2
  62. package/dist/flows/annotate-tools.js +3 -0
  63. package/dist/flows/annotate.js +18 -4
  64. package/dist/flows/coverage.d.ts +38 -5
  65. package/dist/flows/coverage.js +30 -4
  66. package/dist/flows/decision.d.ts +15 -0
  67. package/dist/flows/decision.js +1 -1
  68. package/dist/flows/flow-replay-run.js +8 -1
  69. package/dist/flows/flow-tools.js +9 -1
  70. package/dist/flows/replay.js +3 -0
  71. package/dist/flows/verify-change-tools.js +1 -1
  72. package/dist/honesty/accepted-write.d.ts +3 -0
  73. package/dist/honesty/accepted-write.js +8 -3
  74. package/dist/honesty/already-true.js +15 -0
  75. package/dist/honesty/blind-spots.js +4 -0
  76. package/dist/honesty/honesty.d.ts +11 -0
  77. package/dist/honesty/honesty.js +1 -0
  78. package/dist/honesty/uncaptured-bodies.d.ts +4 -2
  79. package/dist/honesty/uncaptured-bodies.js +9 -2
  80. package/dist/honesty/unread-outcome.d.ts +8 -0
  81. package/dist/honesty/unread-outcome.js +36 -9
  82. package/dist/honesty/unsettled.d.ts +9 -0
  83. package/dist/honesty/unsettled.js +7 -3
  84. package/dist/honesty/verified.d.ts +29 -3
  85. package/dist/honesty/verified.js +67 -8
  86. package/dist/http-server.d.ts +19 -0
  87. package/dist/http-server.js +202 -4
  88. package/dist/impact/impact-recorder.d.ts +23 -0
  89. package/dist/impact/impact-recorder.js +76 -0
  90. package/dist/impact/impact-store.d.ts +47 -0
  91. package/dist/impact/impact-store.js +171 -0
  92. package/dist/index.d.ts +25 -0
  93. package/dist/index.js +60 -10
  94. package/dist/init/agent-rules.d.ts +20 -0
  95. package/dist/init/agent-rules.js +36 -0
  96. package/dist/init/astro-patch.js +27 -1
  97. package/dist/init/confirm.d.ts +21 -3
  98. package/dist/init/confirm.js +32 -5
  99. package/dist/init/connect-steps.d.ts +2 -0
  100. package/dist/init/connect-steps.js +50 -0
  101. package/dist/init/csp-check.d.ts +38 -0
  102. package/dist/init/csp-check.js +92 -0
  103. package/dist/init/csp-doctor.d.ts +25 -0
  104. package/dist/init/csp-doctor.js +49 -0
  105. package/dist/init/cursor.js +6 -9
  106. package/dist/init/detect.d.ts +8 -0
  107. package/dist/init/detect.js +33 -3
  108. package/dist/init/mcp-clients.d.ts +1 -0
  109. package/dist/init/mcp-clients.js +51 -7
  110. package/dist/init/mcp.d.ts +16 -2
  111. package/dist/init/mcp.js +23 -1
  112. package/dist/init/plan-framework.d.ts +10 -0
  113. package/dist/init/plan-framework.js +25 -1
  114. package/dist/init/plan.d.ts +7 -2
  115. package/dist/init/plan.js +66 -30
  116. package/dist/init/run.js +11 -5
  117. package/dist/init/slash-command.d.ts +1 -1
  118. package/dist/init/slash-command.js +12 -4
  119. package/dist/init/snippets.d.ts +1 -1
  120. package/dist/init/snippets.js +35 -7
  121. package/dist/init/vite-config.js +24 -2
  122. package/dist/input/network-mock-tools.js +8 -1
  123. package/dist/input/scroll-find.d.ts +6 -0
  124. package/dist/input/scroll-find.js +17 -2
  125. package/dist/input/scroll-tools.js +4 -0
  126. package/dist/journal/attach-journal.d.ts +9 -1
  127. package/dist/journal/attach-journal.js +13 -2
  128. package/dist/journal/journal-query.d.ts +13 -0
  129. package/dist/journal/journal-query.js +28 -3
  130. package/dist/journal/workspace-gitignore.d.ts +10 -0
  131. package/dist/journal/workspace-gitignore.js +59 -0
  132. package/dist/license/license.d.ts +69 -4
  133. package/dist/license/license.js +84 -15
  134. package/dist/mcp/mcp-proxy.d.ts +7 -93
  135. package/dist/mcp/mcp-proxy.js +183 -250
  136. package/dist/mcp/mcp.d.ts +27 -2
  137. package/dist/mcp/mcp.js +77 -36
  138. package/dist/mcp/proxy-daemon-probe.d.ts +8 -0
  139. package/dist/mcp/proxy-daemon-probe.js +48 -0
  140. package/dist/mcp/proxy-handshake.d.ts +82 -1
  141. package/dist/mcp/proxy-handshake.js +110 -3
  142. package/dist/mcp/proxy-lifecycle.d.ts +10 -1
  143. package/dist/mcp/proxy-lifecycle.js +11 -2
  144. package/dist/mcp/proxy-log.d.ts +63 -0
  145. package/dist/mcp/proxy-log.js +132 -0
  146. package/dist/mcp/server-instructions.d.ts +17 -0
  147. package/dist/mcp/server-instructions.js +56 -0
  148. package/dist/mcp/shared-params.d.ts +27 -0
  149. package/dist/mcp/shared-params.js +36 -0
  150. package/dist/mcp/sse-frame-parser.d.ts +19 -0
  151. package/dist/mcp/sse-frame-parser.js +41 -0
  152. package/dist/project/reticle-dir.d.ts +2 -0
  153. package/dist/project/reticle-dir.js +1 -0
  154. package/dist/session/connection-memory.d.ts +59 -0
  155. package/dist/session/connection-memory.js +108 -0
  156. package/dist/session/dev-command.d.ts +26 -0
  157. package/dist/session/dev-command.js +89 -0
  158. package/dist/session/dev-server-probe.d.ts +68 -0
  159. package/dist/session/dev-server-probe.js +102 -7
  160. package/dist/session/last-act.d.ts +26 -0
  161. package/dist/session/last-act.js +35 -1
  162. package/dist/session/live-control-tools.js +42 -5
  163. package/dist/session/no-session-diagnosis.d.ts +55 -2
  164. package/dist/session/no-session-diagnosis.js +199 -22
  165. package/dist/session/no-session-next-action.d.ts +46 -0
  166. package/dist/session/no-session-next-action.js +85 -0
  167. package/dist/session/no-session-watch.d.ts +40 -1
  168. package/dist/session/no-session-watch.js +172 -11
  169. package/dist/session/pending-commands.d.ts +12 -0
  170. package/dist/session/pending-commands.js +13 -1
  171. package/dist/session/session-manager.d.ts +4 -0
  172. package/dist/session/session-manager.js +35 -5
  173. package/dist/session/session-readiness.js +1 -1
  174. package/dist/session/session.d.ts +10 -1
  175. package/dist/session/session.js +146 -5
  176. package/dist/telemetry/app-instrumented.d.ts +8 -0
  177. package/dist/telemetry/app-instrumented.js +10 -0
  178. package/dist/telemetry/bug-found.d.ts +18 -0
  179. package/dist/telemetry/bug-found.js +14 -0
  180. package/dist/telemetry/daemon-telemetry.js +29 -0
  181. package/dist/telemetry/install-source.d.ts +20 -0
  182. package/dist/telemetry/install-source.js +34 -0
  183. package/dist/telemetry/instrumentation-stall.d.ts +57 -0
  184. package/dist/telemetry/instrumentation-stall.js +103 -0
  185. package/dist/telemetry/license-activation.d.ts +48 -0
  186. package/dist/telemetry/license-activation.js +57 -0
  187. package/dist/telemetry/session-metrics.d.ts +8 -0
  188. package/dist/telemetry/session-metrics.js +10 -0
  189. package/dist/telemetry/telemetry.d.ts +3 -1
  190. package/dist/telemetry/telemetry.js +23 -3
  191. package/dist/tools/act-tools.js +166 -29
  192. package/dist/tools/assert-verdict.js +32 -7
  193. package/dist/tools/browser-tools.js +10 -0
  194. package/dist/tools/dynamic-tools.js +17 -8
  195. package/dist/tools/error-recovery.js +10 -6
  196. package/dist/tools/feedback-tools.js +31 -6
  197. package/dist/tools/invoke-tool.d.ts +1 -1
  198. package/dist/tools/invoke-tool.js +30 -8
  199. package/dist/tools/lease-hint.d.ts +30 -8
  200. package/dist/tools/lease-hint.js +65 -14
  201. package/dist/tools/lease-tools.d.ts +11 -0
  202. package/dist/tools/lease-tools.js +141 -96
  203. package/dist/tools/observe-tools.js +9 -3
  204. package/dist/tools/react-grace.d.ts +4 -0
  205. package/dist/tools/react-grace.js +6 -3
  206. package/dist/tools/read-tools.js +47 -1
  207. package/dist/tools/resolve-target.d.ts +33 -0
  208. package/dist/tools/resolve-target.js +53 -0
  209. package/dist/tools/settle-in-flight.d.ts +15 -0
  210. package/dist/tools/settle-in-flight.js +42 -2
  211. package/dist/tools/snapshot-delta.d.ts +3 -0
  212. package/dist/tools/snapshot-delta.js +67 -6
  213. package/dist/tools/tool-kit.d.ts +38 -3
  214. package/dist/tools/tool-names.d.ts +2 -0
  215. package/dist/tools/tool-names.js +2 -0
  216. package/dist/tools/tool-surface.d.ts +60 -0
  217. package/dist/tools/tool-surface.js +84 -19
  218. package/dist/tools/tools.js +61 -5
  219. package/dist/tools/unadvertised-help.js +9 -6
  220. package/docs/README.md +1 -2
  221. package/docs/actions.mdx +41 -8
  222. package/docs/agent-cheatsheet.md +6 -4
  223. package/docs/architecture.md +6 -0
  224. package/docs/benchmarks.md +60 -12
  225. package/docs/best-practices.mdx +11 -6
  226. package/docs/capabilities.mdx +204 -57
  227. package/docs/cli/affected.mdx +6 -4
  228. package/docs/cli/capsules.mdx +9 -8
  229. package/docs/cli/cloud.mdx +30 -4
  230. package/docs/cli/doctor.mdx +41 -8
  231. package/docs/cli/drive.mdx +34 -15
  232. package/docs/cli/gate.mdx +6 -6
  233. package/docs/cli/help.mdx +8 -6
  234. package/docs/cli/hunt.mdx +20 -4
  235. package/docs/cli/identify.mdx +16 -2
  236. package/docs/cli/init.mdx +35 -8
  237. package/docs/cli/kill.mdx +5 -5
  238. package/docs/cli/license.mdx +1 -1
  239. package/docs/cli/open.mdx +6 -0
  240. package/docs/cli/restart.mdx +6 -6
  241. package/docs/cli/serve.mdx +10 -3
  242. package/docs/cli/status.mdx +29 -5
  243. package/docs/cli/stop.mdx +8 -0
  244. package/docs/cli/verify.mdx +20 -1
  245. package/docs/cli/version.mdx +1 -1
  246. package/docs/cli/watch.mdx +9 -2
  247. package/docs/cli.mdx +44 -22
  248. package/docs/debugging.md +3 -1
  249. package/docs/deploy-checks.md +31 -2
  250. package/docs/desktop.mdx +276 -80
  251. package/docs/docs.json +13 -12
  252. package/docs/enterprise.md +9 -3
  253. package/docs/faq.mdx +49 -35
  254. package/docs/fixtures.md +2 -0
  255. package/docs/flows.md +7 -5
  256. package/docs/for-agents.md +3 -3
  257. package/docs/frameworks.mdx +46 -7
  258. package/docs/gate-plan.md +4 -2
  259. package/docs/gates.md +8 -6
  260. package/docs/getting-started.md +53 -12
  261. package/docs/human-control.md +2 -0
  262. package/docs/index.mdx +43 -1
  263. package/docs/install-agentic.mdx +13 -4
  264. package/docs/install-manual.mdx +6 -4
  265. package/docs/instrumentation.mdx +7 -1
  266. package/docs/integration-patterns.md +3 -1
  267. package/docs/local-registry.md +4 -2
  268. package/docs/multi-agent-testing.md +3 -1
  269. package/docs/packages/babel-plugin.mdx +15 -6
  270. package/docs/packages/browser.mdx +2 -2
  271. package/docs/packages/core.mdx +26 -16
  272. package/docs/packages/electron.mdx +5 -7
  273. package/docs/packages/eslint-plugin.mdx +5 -3
  274. package/docs/packages/next.mdx +3 -5
  275. package/docs/packages/react.mdx +3 -3
  276. package/docs/packages/server.mdx +2 -2
  277. package/docs/packages/tauri.mdx +6 -6
  278. package/docs/packages/test.mdx +18 -7
  279. package/docs/packages/vite-plugin.mdx +4 -6
  280. package/docs/packages.mdx +14 -13
  281. package/docs/platform-integration.md +2 -0
  282. package/docs/predicates.mdx +76 -7
  283. package/docs/quickstart.mdx +51 -5
  284. package/docs/recipes.mdx +63 -23
  285. package/docs/skill-file.mdx +3 -1
  286. package/docs/state-management.mdx +2 -0
  287. package/docs/system-map.md +3 -1
  288. package/docs/telemetry-contract.md +33 -5
  289. package/docs/telemetry-events.mdx +63 -18
  290. package/docs/telemetry.md +8 -5
  291. package/docs/testing.md +35 -1
  292. package/docs/token-efficiency.md +8 -2
  293. package/docs/tools-act-and-wait.mdx +64 -14
  294. package/docs/tools-act-sequence.mdx +34 -17
  295. package/docs/tools-act.mdx +24 -11
  296. package/docs/tools-assert.mdx +69 -10
  297. package/docs/tools-console.mdx +11 -7
  298. package/docs/tools-inspect.mdx +11 -8
  299. package/docs/tools-navigate.mdx +41 -5
  300. package/docs/tools-network.mdx +42 -9
  301. package/docs/tools-observe.mdx +66 -10
  302. package/docs/tools-overview.mdx +26 -23
  303. package/docs/tools-query.mdx +52 -8
  304. package/docs/tools-session-and-feedback.mdx +35 -9
  305. package/docs/tools-sessions.mdx +39 -10
  306. package/docs/tools-snapshot.mdx +57 -3
  307. package/docs/tools-state.mdx +24 -1
  308. package/docs/tools-tools-and-run.mdx +55 -24
  309. package/docs/tools-wait-for.mdx +34 -15
  310. package/docs/troubleshooting.mdx +80 -10
  311. package/docs/usage.md +46 -24
  312. package/docs/vs-chrome-devtools-mcp.mdx +41 -0
  313. package/docs/vs-playwright-mcp.mdx +42 -2
  314. package/docs/vs-screenshots.mdx +42 -1
  315. package/docs/what-is-reticle.mdx +1 -1
  316. package/docs/why-reticle.mdx +3 -1
  317. package/package.json +5 -5
  318. package/docs/desktop-apps.md +0 -265
@@ -4,7 +4,15 @@ description: Every Reticle telemetry event, its payload schema, where it is emit
4
4
  icon: database
5
5
  ---
6
6
 
7
- This is a contributor reference. If you just want to know what Reticle collects and how to turn it off, read [Telemetry](/telemetry) instead; that page is written for people who use Reticle, and this one is written for people who change it.
7
+ <Warning>
8
+ **This page is for contributors, not users.** It documents the internal event *schema* so that
9
+ somebody changing Reticle's code emits the right shape. It contains no data and no measurements.
10
+
11
+ If you want to know what Reticle collects about you and how to turn it off, read [Telemetry](/telemetry) instead. That is the user-facing page, and it is the one with the opt-out.
12
+
13
+ </Warning>
14
+
15
+ Every event Reticle emits is listed below with the same three things: **when it fires**, **what it carries**, and **why it exists**. The authoritative list is `TelemetryEventKind` in `packages/core/src/telemetry.ts`; if this page and that file disagree, the file wins and this page is a bug.
8
16
 
9
17
  Source of truth for everything below:
10
18
 
@@ -16,7 +24,7 @@ Source of truth for everything below:
16
24
 
17
25
  ## The shared envelope
18
26
 
19
- `TelemetryEventSchema` wraps every event. These fields are on all sixteen kinds.
27
+ `TelemetryEventSchema` wraps every event. These fields are on every event kind, whatever it is.
20
28
 
21
29
  | Field | Type | Required | Meaning |
22
30
  | --- | --- | --- | --- |
@@ -24,7 +32,7 @@ Source of truth for everything below:
24
32
  | `anonymousId` | string, 1 to 128 | yes | Random UUID persisted at `~/.reticle/telemetry-id`. Distinct count of this is users |
25
33
  | `projectId` | string, 1 to 128 | no | One-way hash of the git remote, repo root, package root, or cwd. Counts distinct projects without revealing one |
26
34
  | `sessionId` | string, 1 to 64 | no | One daemon run. Present only on session-scoped events |
27
- | `event` | `TelemetryEventKind` | yes | Which of the sixteen |
35
+ | `event` | `TelemetryEventKind` | yes | Which kind this is. The full list is `TelemetryEventKind` in `packages/core/src/telemetry.ts` |
28
36
  | `ts` | int, epoch ms | yes | Client time. The server stamps its own receive time too |
29
37
  | `version` | string, 1 to 64 | yes | The Reticle version emitting |
30
38
  | `ci` | boolean | yes | Running inside CI. Separates human activity from pipeline traffic |
@@ -33,7 +41,9 @@ Source of truth for everything below:
33
41
  | `tzOffsetMin` | int, -900 to 900 | no | Minutes offset from UTC. Turns a time-of-day chart from "which continent" into "9am or 11pm" |
34
42
  | `actor` | `TelemetryActor` | no | `human` or `agent`. Absent on events that are neither |
35
43
 
36
- Then exactly one payload block, keyed by event. The keys are `feedback`, `command` plus `flags`, `session`, `project`, `verification`, `versionChange`, `crash`, `identity`, `connection`, `init`, `bug`, `outage`, `instrumentation`.
44
+ Then exactly one payload block, keyed by event. The keys are `feedback`, `command` plus `flags`, `session`, `project`, `verification`, `versionChange`, `crash`, `identity`, `connection`, `init`, `bug`, `refusal`, `outage`, `instrumentation`, `stall`.
45
+
46
+ Plus `installSource`, which is not a block. It is resolved once per process and stamped on **every** event, so it is the one attribution field you can group any chart by.
37
47
 
38
48
  <Warning>
39
49
  `emit()` builds its wire event from an explicit allowlist of keys. A payload block that is not on
@@ -43,9 +53,11 @@ Then exactly one payload block, keyed by event. The keys are `feedback`, `comman
43
53
 
44
54
  ## Session scope
45
55
 
46
- `isSessionScoped(kind)` decides whether `sessionId` is attached. Eleven kinds are session-scoped:
56
+ `isSessionScoped(kind)` decides whether `sessionId` is attached. Twelve kinds are session-scoped:
47
57
 
48
- `daemon_started`, `daemon_stopped`, `session_progress`, `mcp_client_connected`, `app_instrumented`, `project_profiled`, `verification_completed`, `bug_found`, `tool_refused`, `runtime_crashed`, `feedback_submitted`.
58
+ `daemon_started`, `daemon_stopped`, `session_progress`, `mcp_client_connected`, `app_instrumented`, `instrumentation_stalled`, `project_profiled`, `verification_completed`, `bug_found`, `tool_refused`, `runtime_crashed`, `feedback_submitted`.
59
+
60
+ `mcp_connection_lost` is deliberately **not** on that list even though it feels session-shaped. It fires from the proxy process rather than the daemon, so it carries no `sessionId` and cannot be joined to a session at all.
49
61
 
50
62
  The other six are one-shot. A per-process id on `reticle status` is not a session: it joins to nothing and inflates every session count that runs a distinct count over the field.
51
63
 
@@ -53,7 +65,7 @@ The other six are one-shot. A per-process id on `reticle status` is not a sessio
53
65
 
54
66
  ## reticle_installed
55
67
 
56
- **Wire name:** `reticle_installed`. **Emitted from:** `packages/server/src/telemetry/cli-telemetry.ts:50`. **Session-scoped:** no. **Payload block:** none.
68
+ **Wire name:** `reticle_installed`. **Emitted from:** `packages/server/src/telemetry/cli-telemetry.ts:51`. **Session-scoped:** no. **Payload block:** none.
57
69
 
58
70
  Fires on the first ever run on a machine. Carries `installSource`.
59
71
 
@@ -61,7 +73,7 @@ Fires on the first ever run on a machine. Carries `installSource`.
61
73
  | --- | --- | --- |
62
74
  | `installSource` | `InstallSource`, optional | Which published route brought this install in: `skill_file`, `npx_skill`, `plugin`, `docs_site`, `readme`, `cli_direct`, `unknown` |
63
75
 
64
- `installSource` is **declared, never detected**. A channel sets `RETICLE_INSTALL_SOURCE` on the process that runs the install and anything unrecognised reports `unknown`, so `unknown` is expected to be the largest bucket while the marker spreads across four separately published artifacts. Read a small `unknown` as a marker that spread, never as success. Nothing infers it: `npm_config_user_agent` only says npx ran us and every route goes through npx; the presence of a plugin directory or a skill folder says a route exists rather than that it ran the install. The same field rides on `init_completed`.
76
+ `installSource` is **declared, never detected**. A channel sets `RETICLE_INSTALL_SOURCE` on the process that runs the install and anything unrecognised reports `unknown`, so `unknown` is expected to be the largest bucket while the marker spreads across four separately published artifacts. Read a small `unknown` as a marker that spread, never as success. Nothing infers it: `npm_config_user_agent` only says npx ran us and every route goes through npx; the presence of a plugin directory or a skill folder says a route exists rather than that it ran the install. `installSource` is **not** limited to this event. It is resolved once per process (`packages/server/src/telemetry/telemetry.ts:392`) and stamped on every event that process sends (`:453`), so any chart can group by it. It is also sourced **config first, then environment**: `projectInstallSource()` prefers the value recorded in `.reticle.json`, because the environment variable exists for exactly one command and is gone by the next, which is why the field originally reached a handful of events and nothing else.
65
77
 
66
78
  **The question it answers:** how many machines have installed this, and what does the new-user curve look like? There is deliberately no uninstall event, because npm 7 and pnpm run no uninstall lifecycle script. Churn is inferred server-side from inactivity instead.
67
79
 
@@ -69,7 +81,7 @@ Fires on the first ever run on a machine. Carries `installSource`.
69
81
 
70
82
  ## cli_command_run
71
83
 
72
- **Wire name:** `cli_command_run`. **Emitted from:** `packages/server/src/telemetry/cli-telemetry.ts:52`. **Session-scoped:** no. **Payload:** two envelope fields.
84
+ **Wire name:** `cli_command_run`. **Emitted from:** `packages/server/src/telemetry/cli-telemetry.ts:58`. **Session-scoped:** no. **Payload:** two envelope fields.
73
85
 
74
86
  | Field | Type | Meaning |
75
87
  | --- | --- | --- |
@@ -93,7 +105,7 @@ Fires on the first ever run on a machine. Carries `installSource`.
93
105
 
94
106
  ## daemon_started
95
107
 
96
- **Wire name:** `daemon_started`. **Emitted from:** `packages/server/src/telemetry/daemon-telemetry.ts:73`. **Session-scoped:** yes. **Payload block:** none.
108
+ **Wire name:** `daemon_started`. **Emitted from:** `packages/server/src/telemetry/daemon-telemetry.ts:75`. **Session-scoped:** yes. **Payload block:** none.
97
109
 
98
110
  **The question it answers:** how many active daemon runs are there? This is the numerator of daily, weekly and monthly actives, and the first step of the install funnel.
99
111
 
@@ -101,7 +113,7 @@ Fires on the first ever run on a machine. Carries `installSource`.
101
113
 
102
114
  ## daemon_stopped
103
115
 
104
- **Wire name:** `daemon_stopped`. **Emitted from:** `packages/server/src/telemetry/daemon-telemetry.ts:120`. **Session-scoped:** yes. **Payload block:** `session`, a `SessionSummary` with `final: true`.
116
+ **Wire name:** `daemon_stopped`. **Emitted from:** `packages/server/src/telemetry/daemon-telemetry.ts:138`. **Session-scoped:** yes. **Payload block:** `session`, a `SessionSummary` with `final: true`.
105
117
 
106
118
  The rich one. One event carries the entire session rolled up, replacing the hundreds a per-tool-call event would send. PostHog bills per ingested event and a single verification loop is a great many tool calls, so aggregation here is a design decision, not an optimisation.
107
119
 
@@ -150,7 +162,7 @@ The rich one. One event carries the entire session rolled up, replacing the hund
150
162
 
151
163
  ## session_progress
152
164
 
153
- **Wire name:** `session_progress`. **Emitted from:** `packages/server/src/telemetry/daemon-telemetry.ts:98`. **Session-scoped:** yes. **Payload block:** `session`, the same `SessionSummary` shape with `final: false` and no `exit`.
165
+ **Wire name:** `session_progress`. **Emitted from:** `packages/server/src/telemetry/daemon-telemetry.ts:116`. **Session-scoped:** yes. **Payload block:** `session`, the same `SessionSummary` shape with `final: false` and no `exit`.
154
166
 
155
167
  A periodic roll-up from a daemon that is still running.
156
168
 
@@ -167,7 +179,7 @@ A periodic roll-up from a daemon that is still running.
167
179
 
168
180
  ## verification_completed
169
181
 
170
- **Wire name:** `verification_completed`. **Emitted from:** `packages/server/src/tools/invoke-tool.ts:125` and `packages/server/src/telemetry/run-telemetry.ts:38`. **Session-scoped:** yes. **Payload block:** `verification`.
182
+ **Wire name:** `verification_completed`. **Emitted from:** `packages/server/src/tools/invoke-tool.ts:144` and `packages/server/src/telemetry/run-telemetry.ts:39`. **Session-scoped:** yes. **Payload block:** `verification`.
171
183
 
172
184
  ### `VerificationSchema`
173
185
 
@@ -189,7 +201,7 @@ A periodic roll-up from a daemon that is still running.
189
201
 
190
202
  ## project_profiled
191
203
 
192
- **Wire name:** `project_profiled`. **Emitted from:** `packages/server/src/telemetry/daemon-telemetry.ts:87`. **Session-scoped:** yes. **Payload block:** `project`. Once per daemon start, so it is cheap.
204
+ **Wire name:** `project_profiled`. **Emitted from:** `packages/server/src/telemetry/daemon-telemetry.ts:91`. **Session-scoped:** yes. **Payload block:** `project`. Once per daemon start, so it is cheap.
193
205
 
194
206
  ### `ProjectProfileSchema`
195
207
 
@@ -307,7 +319,7 @@ The one kind that carries author-written free text, which is exactly why it is n
307
319
  | --- | --- | --- |
308
320
  | `reconnect` | boolean | False on the first attach to this daemon, true for every attach after |
309
321
  | `daemonAgeMs` | int | How long the daemon had been up first |
310
- | `client` | string, optional | From the handshake: `claude-code`, `cursor` |
322
+ | `client` | string, optional | **Never populated today.** The schema defines it and `mcp-connection.ts:35` would spread it, but the only caller (`http-server.ts:202`) passes no argument, so it is absent on every event ever sent. The client name and version _are_ captured, at `mcp/mcp.ts:461` and `:471` on `oninitialized`, and land in `session.clients` / `session.clientVersions`. Per-connect client attribution is therefore only reachable by joining to the session summary on `sessionId`. |
311
323
 
312
324
  **The question it answers:** is somebody actually using this, as opposed to having it installed and running? A daemon can sit up for days with no agent attached. A large `daemonAgeMs` on the **first** connect is the interesting case: Reticle was started and then sat unused, which is an onboarding failure nobody would otherwise report. Reconnect churn is visible here too, and a client reattaching every few minutes looks identical to healthy usage in every other metric.
313
325
 
@@ -329,6 +341,39 @@ Fired **once per daemon run**, on the first connect only, so `daemon_started` to
329
341
 
330
342
  ---
331
343
 
344
+ ## instrumentation_stalled
345
+
346
+ **Wire name:** `instrumentation_stalled`. **Emitted from:** `packages/server/src/telemetry/instrumentation-stall.ts:73`, called from `daemon-telemetry.ts:105`. **Session-scoped:** yes. **Payload block:** `stall`.
347
+
348
+ The failure side of `app_instrumented`, and the reason the funnel has a denominator at all. A daemon that runs for ten minutes without a single SDK-carrying page is an install that half worked, and until this event existed that outcome was pure silence: indistinguishable from a machine nobody touched.
349
+
350
+ | Field | Type | Meaning |
351
+ | --- | --- | --- |
352
+ | `initialized` | boolean | Has `init` run in this working directory (`readProjectId(cwd)` resolves). False means the daemon is running somewhere `init` never wrote a project id, which is usually a wrong-directory problem rather than a wiring one |
353
+ | `agentAttached` | boolean | Was an agent connected at the moment the check ran. **This is the field that decides whether the row is a defect.** False means somebody installed and walked away, which is not a bug. True means an agent was sitting there with tools it could not use, which is the failure the product actually has |
354
+ | `msWaited` | int | Milliseconds since the daemon started, from the clock marked at `daemon-telemetry.ts:84` |
355
+
356
+ Deliberately the same three facts as `app_instrumented`, with `msWaited` standing in for `msToFirstApp`, so the success and failure rows are directly comparable without reshaping either.
357
+
358
+ **When it fires:** on every flush tick, but only once all four guards at `instrumentation-stall.ts:64` pass: it has not already been reported this run, no app has ever connected, the clock was marked, and at least `STALL_AFTER_MS` (10 minutes) has elapsed. It is checked **before** the empty-window early return in `flushWindow`, which is not an accident: a stalled install produces an empty window by definition, so checking after the return would blind the event to the exact condition it exists to report.
359
+
360
+ **The question it answers:** of the installs that never reach a verdict, how many had an agent present and waiting? That is the difference between a distribution problem and a wiring problem, and they need opposite responses.
361
+
362
+ **What it is blind to:**
363
+
364
+ - **Any diagnosis.** It never says whether a dev server is listening, because that is a live probe, and probing a user's machine to enrich a metric is collecting for the metric's sake.
365
+ - **Slow starts.** A run that connects at minute eleven reports nothing at all, and neither does the recovery. The event answers "did this run stall", not "how long do installs take".
366
+ - **The page side.** Every real cause (a non-localhost gate, a port mismatch, a stale build, a dev server never restarted) lives where the daemon cannot see. Attributing a stall needs the SDK to report its own refusals, and that is not built.
367
+ - **Repeats.** Once per daemon run, so a user stuck for a week contributes one row per daemon start, not one per hour.
368
+
369
+ <Warning>
370
+ This kind has no live check in `apps/e2e/specs/telemetry-events-test.mjs`, and neither do
371
+ `session_progress` and `app_instrumented`. Those three are the funnel, and the funnel is the part
372
+ of the wire nothing fires end to end. Adding one is the highest-value thing left in this file.
373
+ </Warning>
374
+
375
+ ---
376
+
332
377
  ## mcp_connection_lost
333
378
 
334
379
  **Wire name:** `mcp_connection_lost`. **Emitted from:** `packages/server/src/mcp/mcp-outage.ts:53`. **Session-scoped:** no. **Payload block:** `outage`.
@@ -348,7 +393,7 @@ Fired **once per daemon run**, on the first connect only, so `daemon_started` to
348
393
 
349
394
  ## init_completed
350
395
 
351
- **Wire name:** `init_completed`. **Emitted from:** `packages/server/src/telemetry/init-telemetry.ts:31`. **Session-scoped:** no. **Payload block:** `init`.
396
+ **Wire name:** `init_completed`. **Emitted from:** `packages/server/src/telemetry/init-telemetry.ts:32`. **Session-scoped:** no. **Payload block:** `init`.
352
397
 
353
398
  | Field | Type | Meaning |
354
399
  | --- | --- | --- |
@@ -363,7 +408,7 @@ Fired **once per daemon run**, on the first connect only, so `daemon_started` to
363
408
 
364
409
  ## bug_found
365
410
 
366
- **Wire name:** `bug_found`. **Emitted from:** `packages/server/src/tools/invoke-tool.ts:147` and `packages/server/src/telemetry/run-telemetry.ts:58`. **Session-scoped:** yes. **Payload block:** `bug`.
411
+ **Wire name:** `bug_found`. **Emitted from:** `packages/server/src/tools/invoke-tool.ts:166` and `packages/server/src/telemetry/run-telemetry.ts:59`. **Session-scoped:** yes. **Payload block:** `bug`.
367
412
 
368
413
  | Field | Type | Meaning |
369
414
  | --- | --- | --- |
@@ -388,7 +433,7 @@ The payload never carries a selector, a URL, an element, or any description of t
388
433
 
389
434
  ## tool_refused
390
435
 
391
- **Wire name:** `tool_refused`. **Emitted from:** `packages/server/src/tools/invoke-tool.ts`, via `packages/server/src/telemetry/tool-refused.ts`. **Session-scoped:** yes. **Payload block:** `refusal`.
436
+ **Wire name:** `tool_refused`. **Emitted from:** `packages/server/src/telemetry/tool-refused.ts:49`, reached from four call sites in `packages/server/src/tools/invoke-tool.ts` (`:285` a handler threw, `:325` the wrong-tab guard, `:346` a message-shaped refusal, `:376` the `{error}` return convention). **Session-scoped:** yes. **Payload block:** `refusal`.
392
437
 
393
438
  | Field | Type | Meaning |
394
439
  | --- | --- | --- |
package/docs/telemetry.md CHANGED
@@ -4,7 +4,7 @@ description: 'The complete description of the anonymous usage data Reticle colle
4
4
  icon: chart-line
5
5
  ---
6
6
 
7
- Reticle collects a small amount of anonymous usage data to help us understand whether the tool is useful: which commands people run, which tools agents actually use, and whether people keep using Reticle after they try it. That's what this data is for, and all it is for: making the product better.
7
+ Reticle sends anonymous usage counters (which commands ran, which tools an agent called, whether a verification produced a verdict) and nothing from the app under test. To turn it off permanently, run `npx @reticlehq/server telemetry disable`.
8
8
 
9
9
  This page is the complete description of what is collected. If something is not listed here, it is not sent.
10
10
 
@@ -21,7 +21,7 @@ This page is the complete description of what is collected. If something is not
21
21
 
22
22
  ## What is sent
23
23
 
24
- Thirteen kinds of events, each a single small JSON object:
24
+ Eighteen kinds of events, each a single small JSON object. This is the whole list, and it is the same list the code enumerates:
25
25
 
26
26
  | Event | When | Extra data |
27
27
  | --- | --- | --- |
@@ -29,12 +29,15 @@ Thirteen kinds of events, each a single small JSON object:
29
29
  | `cli_command_run` | You run a `reticle` command | Which subcommand (`verify`, `status`, …) and which flags were present, by name |
30
30
  | `daemon_started` | The local daemon starts | n/a |
31
31
  | `daemon_stopped` | The local daemon stops | A summary of the session; see below |
32
+ | `session_progress` | Periodically, from a daemon that is still running | The same summary as `daemon_stopped`, marked `final: false`. It exists so a long session is not invisible until it exits |
33
+ | `app_instrumented` | The first time your app's SDK connects in a given daemon run | Nothing beyond the common fields. It is the second half of the install funnel: registering the MCP server is one thing, getting the SDK into a running page is another |
34
+ | `instrumentation_stalled` | Ten minutes into a daemon run in which your app has still never connected | The same three facts as `app_instrumented`, with how long it waited instead of how long it took. It fires at most once per daemon run, and a run that starts slowly and then works reports nothing at all. It carries no diagnosis: whether a dev server is listening would need a live probe, and probing your machine to enrich a metric is collecting for the metric's sake |
32
35
  | `verification_completed` | A verification produces a verdict | Whether it passed, whether Reticle refused to call a passing check verified, and **why** the verdict came out that way; see below |
33
36
  | `project_profiled` | Once per daemon start | The shape of the project; see below |
34
37
  | `version_changed` | You update or roll back | The two version numbers, and which direction |
35
38
  | `runtime_crashed` | The daemon hits an uncaught error | The error's type, **Reticle's own** stack frames, and the message with variables stripped; see below |
36
39
  | `mcp_client_connected` | An agent attaches to the daemon | Whether it is a reconnect, and how long the daemon had been idle |
37
- | `mcp_connection_lost` | The agent's MCP tools go away | Which stage (`first`, or `budget_spent` when it stopped retrying), the cause, and the attempt count. **At most twice per session**: one measured afternoon produced 547 reconnects, and an event each would bill for the pathology instead of measuring it |
40
+ | `mcp_connection_lost` | The agent's MCP tools go away | Which stage (`first`, or `budget_spent` when it stopped retrying), the cause, and the attempt count. **At most twice per session**, because a reconnect loop can fire many times in a row and an event each would bill for the pathology instead of measuring it |
38
41
  | `init_completed` | `reticle init` finishes | Whether it worked, a classified reason when it did not, and which published install route you came through, DECLARED by that route via `RETICLE_INSTALL_SOURCE` and never inferred, and `unknown` when nothing declared one |
39
42
  | `bug_found` | Reticle finds a defect in the app under test | The **kind** of defect (`signal-contradicted`, `console-error`, …) and how it was found, never what it was found in |
40
43
  | `tool_refused` | A tool cannot do what an agent asked | Which tool, why (one of six buckets: no session, no match, unsupported, bad arguments, not ready, other), and whether the same call was retried. **Never the message**, because a refusal interpolates whatever you asked for, so only the tool name and the bucket are sent. Capped at 50 per daemon run |
@@ -51,7 +54,7 @@ It also carries a snapshot of the **machine's** state (our own process's memory,
51
54
 
52
55
  ### Why a verdict came out that way
53
56
 
54
- `verification_completed` carries the **clause** that decided the verdict, from a fixed list we define: `proved`, `contradicted`, `assertion_failed`, `already_true`, `unclean_capture`, `vacuous_grade`, `outcome_pending`, `outcome_unread`, `unsettled`, `observation_lost`, `inconclusive`.
57
+ `verification_completed` carries the **clause** that decided the verdict, from a fixed list we define: `proved`, `contradicted`, `assertion_failed`, `already_true`, `unclean_capture`, `vacuous_grade`, `outcome_pending`, `outcome_unread`, `unsettled`, `evidence_incomplete`, `observation_lost`, `inconclusive`.
55
58
 
56
59
  It exists because `verified: "unknown"` covered seven different situations belonging to three different owners (your app, your agent, and Reticle's own blind spots), and they arrived as one value. It is a **name from our own vocabulary**, never a description of your app: `contradicted` says two channels disagreed, not which ones, about what, or on which page.
57
60
 
@@ -139,7 +142,7 @@ Alongside the report, a feedback event carries context about the environment it
139
142
  | `driver` | `cdp`, `sdk` | Whether Reticle drove the page or observed your own browser |
140
143
  | `client` | `claude-code`, `cursor` | The MCP client's own name from its handshake |
141
144
  | `mcpScope` | `user`, `project` | How Reticle is registered |
142
- | `kind` | `bug`, `gap`, `ambiguity`, `experience` | A defect, a blind spot, an undecidable verdict, or an overall take |
145
+ | `kind` | `bug`, `gap`, `ambiguity`, `feature_request`, `improvement`, `experience` | A defect, a blind spot, an undecidable verdict, a thing that does not exist, a thing that exists but is awkward, or a human's overall take |
143
146
 
144
147
  Plus the `version`, `os`, and `ci` fields every event carries. No paths, no project name, no dependency list, no user-agent string.
145
148
 
package/docs/testing.md CHANGED
@@ -4,7 +4,9 @@ description: 'Turn an interactive drive into a repeatable suite with @reticlehq/
4
4
  icon: vial
5
5
  ---
6
6
 
7
- Driving Reticle interactively is reconnaissance. To turn it into a **repeatable, CI-runnable** suite, write declarative specs with `@reticlehq/test`. Specs bind to **signals and testids, never DOM structure**, so they inherit Reticle's refactor-resistance.
7
+ To run Reticle checks in CI, install `@reticlehq/test`, write specs with `reticleTest(name, async (t) => …)`, then boot a headless session with `bootSession({ driveUrl, headless: true })` and run them with `runSpecs`. Exit non-zero when `summary.failed` is not `0`. Specs bind to **signals and testids, never DOM structure**, so they survive the refactors that break selector-based suites.
8
+
9
+ Driving Reticle interactively is reconnaissance. This page is how you make it repeatable.
8
10
 
9
11
  ```ts
10
12
  import { reticleTest } from '@reticlehq/test';
@@ -84,3 +86,35 @@ import { flowsAsSpecs } from '@reticlehq/test';
84
86
  ## Authoring tip: record → prune → commit
85
87
 
86
88
  You don't have to hand-write steps. Drive the flow once (or record it via the panel), let Reticle emit the program, trim it, and commit it as a spec. The regression test is a byproduct of testing, not separate work.
89
+
90
+ ## FAQ
91
+
92
+ ### Do I need vitest or jest to run these?
93
+
94
+ No. `runSpecs` is its own runner: it takes the specs registered by `reticleTest`, an `invoke` function, and a `print` callback, and returns `{ results, summary }`. You can call it from a plain `node script.mjs`. If you already have vitest, nothing stops you calling `runSpecs` from inside a test, but the runner does not depend on one.
95
+
96
+ ### How do I fail the CI job?
97
+
98
+ `runSpecs` never throws on a failing spec, so you have to read the summary and exit yourself:
99
+
100
+ ```ts
101
+ process.exit(summary.failed === 0 ? 0 : 1);
102
+ ```
103
+
104
+ `summary` carries `{ total, passed, failed, skipped, ok }`. Note that **skipped is not failed**: a spec that skipped with a reason (see `t.expectInputModeReal()`) leaves `failed` at `0`.
105
+
106
+ ### Why did my hover or drag spec skip instead of running?
107
+
108
+ Because native input was not active. `t.expectInputModeReal()` deliberately skips with a reason rather than passing on a synthetic no-op, since a synthetic hover cannot trigger a CSS `:hover` or a pointer-library drag. If you want those specs to actually run, drive the app with real input: `npx @reticlehq/server drive http://localhost:4310`, or point the server at a CDP endpoint with `RETICLE_CDP_URL`.
109
+
110
+ ### Does the app need to be running already?
111
+
112
+ Yes. The spec runner is attach-only and never starts your dev server (that is the interactive agent's job, not CI's). Boot your app first, then pass its URL as `driveUrl`. If nothing is listening there, `bootSession` opens a headless tab against a URL that serves nothing and no session ever connects.
113
+
114
+ ### Can I emit JUnit for my CI's test reporter?
115
+
116
+ Yes, `toJUnitXml` and `writeJUnit` are both exported from `@reticlehq/test`. Pass them the results array from `runSpecs`.
117
+
118
+ ### Do I have to write specs by hand if I already have flows?
119
+
120
+ No. `flowsAsSpecs` registers one `reticleTest` per flow under `.reticle/flows/`, replayed with that flow's own `expect` and `success` predicates and skipping its `dynamic` regions. That keeps the recorded map and the CI suite from drifting apart, because they are the same artifact.
@@ -4,6 +4,8 @@ description: Why asking narrow questions costs a fraction of feeding the whole a
4
4
  icon: coins
5
5
  ---
6
6
 
7
+ A full Reticle verify loop costs about **100 tokens**, against roughly **7,300** for a full-tree accessibility snapshot on the same page: about 73x leaner, because Reticle asks a narrow question instead of returning the page. If you re-look after an action with `reticle_snapshot({ diff: true })`, the re-read costs about 99% less again.
8
+
7
9
  Agent browser tools that feed the **whole accessibility tree** to the model every step get expensive fast. Playwright MCP's own ecosystem notes its snapshots _"can exceed 50,000 tokens on complex pages,"_ with a _typical task ~114,000 tokens through MCP._ Reticle is built to ask **narrow questions** instead, so the per-interaction cost stays tiny.
8
10
 
9
11
  ## Head-to-head (measured, same page, same moment)
@@ -33,7 +35,7 @@ Measured on a representative 150-row dashboard (the shipped regression benchmark
33
35
  | Payload | Tokens |
34
36
  | ---------------------------------- | --------: |
35
37
  | Full re-snapshot (150-row table) | **4,246** |
36
- | `diff:true` after a one-row change | **60** |
38
+ | `diff:true` after a one-row change | **65** |
37
39
  | `diff:true` when nothing changed | **17** |
38
40
 
39
41
  **~99% fewer tokens** to re-look after an action, and because a `delta` carries no stale full tree, it also removes the 60K to 80K-token stale-context buildup that makes long-running agents start hallucinating selectors that no longer exist.
@@ -53,9 +55,13 @@ Measured live, all servers in one run, same tokenizer (`bench/harness/schema-tax
53
55
  | Chrome DevTools MCP | 29 | 5,116 |
54
56
  | Reticle, `RETICLE_ADVERTISE_ALL_TOOLS=1` | 48 | ~30,200 |
55
57
 
58
+ _Measured 2026-08-12 (`bench/raw/schema-tax.json`). Reticle's default surface has gained a tool since, so treat the first row as a floor._
59
+
56
60
  There is one tool surface: the verify loop advertised directly, plus two meta-tools (`reticle_tools`, `reticle_run`) that reach every other tool on demand. Nothing is unreachable; the cold tail simply is not re-sent every turn.
57
61
 
58
- `RETICLE_ADVERTISE_ALL_TOOLS=1` advertises everything WITH output schemas. It is a verification switch for suites that call by name, not a mode to run agents in. It is roughly 7x the per-turn cost, which is why it is opt-in: measured, carrying output schemas on the default surface takes it from 18,183 to 41,117 bytes. (That 18,183 was taken when the surface was 16 tools; a fresh `tools/list` read on 2026-08-14 puts the current 18-tool surface at **21,468 bytes**, and all 48 with output schemas at **134,368**. The shape of the gap holds; the absolute numbers move every time a tool is added, which is why they are asserted in `surface-sizes.test.ts` rather than trusted from prose.)
62
+ `RETICLE_ADVERTISE_ALL_TOOLS=1` advertises everything WITH output schemas. It is a verification switch for suites that call by name, not a mode to run agents in. It is roughly 7x the per-turn cost, which is why it is opt-in.
63
+
64
+ **Every absolute number in this section is a dated reading, not a current value.** The table above was measured on 2026-08-12; a later `tools/list` read on 2026-08-14 put the default surface at 21,468 bytes and the full one at 134,368. The surface has grown a tool since, so both readings are already low. The _shape_ of the gap is the durable claim; the counts themselves are asserted by `surface-sizes.test.ts`, which reads them off the live surface, and that is the only place to trust them.
59
65
 
60
66
  The typed result object still travels as `structuredContent` either way; the default surface simply does not advertise the output schema, which an agent reading the `text` block never consumed.
61
67
 
@@ -4,6 +4,8 @@ description: Act on an element and wait for a named consequence in one call. Thi
4
4
  icon: circle-check
5
5
  ---
6
6
 
7
+ `reticle_act_and_wait` performs one action on an element and waits for a named consequence, returning a verdict in the same call. Reach for it whenever an action is supposed to cause something: it is one of only two Reticle tools that produce a verdict, the other being [`reticle_assert`](/tools-assert).
8
+
7
9
  This is the tool Reticle exists for. It does the act, observe and assert steps in one round trip, and it makes you name the expected consequence **before** the action runs.
8
10
 
9
11
  That ordering is not a convenience. An agent that acts first and then decides what counts as success will always find something that counts. Naming the consequence up front is the difference between a check and a rationalisation.
@@ -12,13 +14,13 @@ That ordering is not a convenience. An agent that acts first and then decides wh
12
14
 
13
15
  ```json
14
16
  {
15
- "ref": "e5",
17
+ "ref": "e105",
16
18
  "action": "click",
17
19
  "until": { "kind": "net", "method": "POST" }
18
20
  }
19
21
  ```
20
22
 
21
- Real response, trimmed:
23
+ Real response. The `initiatorStack`, `storageDiffs` and per-event trace detail are trimmed out for length; nothing else is changed:
22
24
 
23
25
  ```json
24
26
  {
@@ -27,35 +29,73 @@ Real response, trimmed:
27
29
  "because": "assertion held at presence grade over a clean capture with no channel disagreeing",
28
30
  "effect": {
29
31
  "ok": true,
32
+ "ref": "e105",
33
+ "action": "click",
34
+ "dispatched": true,
35
+ "settled": true,
36
+ "settleReason": null,
30
37
  "testid": "login-submit",
31
38
  "component": "Login",
32
- "effect": { "domMutatedWithin": 7, "appeared": "Signing in…" },
39
+ "role": "button",
40
+ "name": "Sign in",
41
+ "effect": { "focusMoved": "e104->null", "domMutatedWithin": 8, "appeared": "Signing in…" },
33
42
  "source": { "file": "src/components/Login.tsx", "line": 81, "column": 8 }
34
43
  },
35
44
  "verdict": {
36
45
  "pass": true,
37
46
  "evidence": {
47
+ "id": "n2",
38
48
  "method": "POST",
39
49
  "url": "http://localhost:8787/api/login",
40
50
  "status": 200,
41
- "durationMs": 67,
42
- "requestBody": "{\"email\":\"admin@reticle.dev\",\"password\":\"[REDACTED]\"}"
51
+ "ok": true,
52
+ "durationMs": 4,
53
+ "initiator": "fetch",
54
+ "statusText": "OK",
55
+ "contentType": "application/json; charset=utf-8",
56
+ "responseSize": 67,
57
+ "requestBody": "{\"email\":\"admin@reticle.dev\",\"password\":\"[REDACTED]\"}",
58
+ "responseBody": "{\"token\":\"[REDACTED]\",\"user\":{\"email\":\"admin@reticle.dev\"}}"
59
+ }
60
+ },
61
+ "trace": {
62
+ "window_ms": 31,
63
+ "summary": {
64
+ "total": 21,
65
+ "network": 1,
66
+ "domAdded": 0,
67
+ "domRemoved": 1,
68
+ "domChanged": 6,
69
+ "routeChanges": 0,
70
+ "consoleErrors": 0,
71
+ "animations": 0,
72
+ "signals": 1
43
73
  }
44
74
  },
45
75
  "summary": {
46
76
  "net": { "total": 1, "errors": 0 },
47
77
  "consoleErrors": 0,
48
- "stateDiffs": [{ "path": "auth", "from": null, "to": "{\"email\":\"admin@reticle.dev\"}" }],
49
- "signals": ["auth:granted"]
78
+ "statePathsChanged": ["app"],
79
+ "storageKeysChanged": ["reticle.bench.authToken", "reticle.bench.sessionId"],
80
+ "stateDiffs": [
81
+ { "path": "auth", "from": null, "to": "{\"email\":\"admin@reticle.dev\"}", "atMs": 44403 }
82
+ ],
83
+ "signals": ["auth:granted"],
84
+ "layoutShift": 0.016486682891845704,
85
+ "longTasks": 0
50
86
  },
51
87
  "honesty": {
52
88
  "grade": "presence",
89
+ "attribution": "window",
53
90
  "coverage": { "pct": 100, "partial": false },
54
91
  "integrity": { "clean": true, "issues": [] }
55
- }
92
+ },
93
+ "since": 44395
56
94
  }
57
95
  ```
58
96
 
97
+ `trace` is a digest of what the app did in the window. When the counts are not enough, pass the top-level `since` cursor to [`reticle_observe`](/tools-observe) for the per-event timeline.
98
+
59
99
  ## The predicate grammar
60
100
 
61
101
  `until` takes `{ kind, ...fields }`. The kinds:
@@ -115,9 +155,19 @@ A verification tool that cannot describe the quality of its own evidence is aski
115
155
 
116
156
  ## Arguments
117
157
 
118
- | Argument | What it does |
119
- | ----------------------- | ---------------------------------------- |
120
- | `ref`, `action`, `args` | As [`reticle_act`](/tools-act) |
121
- | `until` | The predicate to wait for |
122
- | `timeout_ms` | Maximum wait |
123
- | `refuseWhenThrottled` | Throw rather than act on a throttled tab |
158
+ | Argument | What it does |
159
+ | --- | --- |
160
+ | `ref`, `action`, `args` | As [`reticle_act`](/tools-act) |
161
+ | `until` | The predicate to wait for. Omit it to wait for the page to settle |
162
+ | `predicate` | Alias for `until`, the name `assert` and `wait_for` use |
163
+ | `timeout_ms` | Maximum wait, defaulting to 4000. `0` evaluates the predicate once, no wait |
164
+ | `refuseWhenThrottled` | Throw rather than act on a throttled tab |
165
+
166
+ Omitting `until` waits for network and DOM idle, which is the deterministic replacement for a fixed sleep. To assert a consequence **and** settle, combine them:
167
+
168
+ ```json
169
+ {
170
+ "kind": "allOf",
171
+ "predicates": [{ "kind": "signal", "name": "deploy:created" }, { "kind": "settled" }]
172
+ }
173
+ ```
@@ -4,6 +4,8 @@ description: Run several actions in order in one round trip. Fill, fill, submit.
4
4
  icon: layer-group
5
5
  ---
6
6
 
7
+ `reticle_act_sequence` runs several actions in order in a single round trip. Reach for it whenever a journey needs more than one step, such as fill, fill, submit, and then prove the outcome once with [`reticle_act_and_wait`](/tools-act-and-wait), because a sequence itself produces no verdict.
8
+
7
9
  A login is three actions. Driving it one call at a time costs three round trips and three turns of context. `reticle_act_sequence` does it in one.
8
10
 
9
11
  This tool exists because of a measured problem: agents were driving forms one call at a time, over and over. Those repeats were not retries. The calls succeeded and got repeated, because the batching tool was not advertised and so nobody knew it was there.
@@ -13,18 +15,18 @@ This tool exists because of a measured problem: agents were driving forms one ca
13
15
  ```json
14
16
  {
15
17
  "steps": [
16
- { "ref": "e101", "action": "fill", "args": { "value": "admin@reticle.dev" } },
17
- { "ref": "e102", "action": "fill", "args": { "value": "correct-horse-battery-staple" } },
18
- { "ref": "e103", "action": "click" }
18
+ { "ref": "e603", "action": "fill", "args": { "value": "admin@reticle.dev" } },
19
+ { "ref": "e604", "action": "fill", "args": { "value": "correct-horse-battery-staple" } },
20
+ { "ref": "e605", "action": "click" }
19
21
  ]
20
22
  }
21
23
  ```
22
24
 
23
- Real response, trimmed:
25
+ Real response. Each `effects[]` entry also carries `targetMatched`, `visible`, `enabled`, `defaultPrevented`, `occluded`, `occludedBy` and `scrolledIntoView`, and each `steps[]` entry also carries `dispatched`, `settled`, `settleReason`, `component`, `role` and `name`. Those are trimmed here for length:
24
26
 
25
27
  ```json
26
28
  {
27
- "since": 13776,
29
+ "since": 3915,
28
30
  "dispatched": true,
29
31
  "result": {
30
32
  "ok": true,
@@ -32,38 +34,38 @@ Real response, trimmed:
32
34
  "effects": [
33
35
  {
34
36
  "dispatched": true,
35
- "focusMoved": "null->e101",
37
+ "focusMoved": "null->e603",
36
38
  "valueChanged": false,
37
- "domMutatedWithin": 6
39
+ "domMutatedWithin": 0
38
40
  },
39
41
  {
40
42
  "dispatched": true,
41
- "focusMoved": "e101->e102",
43
+ "focusMoved": "e603->e604",
42
44
  "valueChanged": true,
43
- "domMutatedWithin": 10
45
+ "domMutatedWithin": 9
44
46
  },
45
47
  {
46
48
  "dispatched": true,
47
- "focusMoved": "e102->null",
49
+ "focusMoved": "e604->null",
48
50
  "domMutatedWithin": 15,
49
51
  "appeared": "Signing in… | Invalid email or password | Sign in"
50
52
  }
51
53
  ],
52
54
  "steps": [
53
55
  {
54
- "ref": "e101",
56
+ "ref": "e603",
55
57
  "action": "fill",
56
58
  "testid": "login-email",
57
59
  "source": { "file": "src/components/Login.tsx", "line": 48 }
58
60
  },
59
61
  {
60
- "ref": "e102",
62
+ "ref": "e604",
61
63
  "action": "fill",
62
64
  "testid": "login-password",
63
65
  "source": { "file": "src/components/Login.tsx", "line": 59 }
64
66
  },
65
67
  {
66
- "ref": "e103",
68
+ "ref": "e605",
67
69
  "action": "click",
68
70
  "testid": "login-submit",
69
71
  "source": { "file": "src/components/Login.tsx", "line": 81 }
@@ -95,8 +97,8 @@ The honest pattern is: batch the setup, prove the outcome.
95
97
  ```json
96
98
  {
97
99
  "steps": [
98
- { "ref": "e101", "action": "fill", "args": { "value": "admin@reticle.dev" } },
99
- { "ref": "e102", "action": "fill", "args": { "value": "reticle" } }
100
+ { "ref": "e603", "action": "fill", "args": { "value": "admin@reticle.dev" } },
101
+ { "ref": "e604", "action": "fill", "args": { "value": "correct-password" } }
100
102
  ]
101
103
  }
102
104
  ```
@@ -104,10 +106,25 @@ The honest pattern is: batch the setup, prove the outcome.
104
106
  then
105
107
 
106
108
  ```json
107
- { "ref": "e103", "action": "click", "until": { "kind": "signal", "name": "auth:granted" } }
109
+ { "ref": "e605", "action": "click", "until": { "kind": "signal", "name": "auth:granted" } }
110
+ ```
111
+
112
+ which returns the verdict the sequence could not:
113
+
114
+ ```json
115
+ {
116
+ "verified": "yes",
117
+ "verifiedReason": "proved",
118
+ "because": "assertion held at signal grade over a clean capture with no channel disagreeing",
119
+ "verdict": {
120
+ "pass": true,
121
+ "evidence": { "name": "auth:granted", "data": { "email": "admin@reticle.dev" } }
122
+ },
123
+ "honesty": { "grade": "signal", "coverage": { "pct": 100, "partial": false } }
124
+ }
108
125
  ```
109
126
 
110
- Two calls instead of four, and the last one returns a verdict.
127
+ Two calls instead of four, and the last one returns a verdict. Note `grade: "signal"`, the strongest Reticle awards, because the app said so itself.
111
128
 
112
129
  ## What each step reports
113
130