@reticlehq/server 2.6.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/README.md +14 -5
  2. package/SKILL.md +251 -33
  3. package/dist/bridge/bridge.d.ts +6 -1
  4. package/dist/bridge/bridge.js +27 -4
  5. package/dist/capsule/causal-summary.d.ts +20 -0
  6. package/dist/capsule/causal-summary.js +31 -6
  7. package/dist/capsule/divergence.js +7 -1
  8. package/dist/cli/chromium-hint.d.ts +68 -0
  9. package/dist/cli/chromium-hint.js +145 -0
  10. package/dist/cli/cli-doctor.js +31 -36
  11. package/dist/cli/cli-kill.d.ts +58 -0
  12. package/dist/cli/cli-kill.js +162 -0
  13. package/dist/cli/cli-launch.d.ts +18 -0
  14. package/dist/cli/cli-launch.js +32 -1
  15. package/dist/cli/cli-parse.d.ts +16 -1
  16. package/dist/cli/cli-parse.js +32 -9
  17. package/dist/cli/cli-port.d.ts +8 -0
  18. package/dist/cli/cli-port.js +36 -1
  19. package/dist/cli/cli-verify.js +2 -2
  20. package/dist/cli/doctor-daemon-line.js +10 -1
  21. package/dist/cli/doctor-sessions-line.d.ts +12 -0
  22. package/dist/cli/doctor-sessions-line.js +66 -0
  23. package/dist/cli/port-holder.d.ts +10 -0
  24. package/dist/cli/port-holder.js +26 -2
  25. package/dist/cli.js +139 -23
  26. package/dist/crawl/crawl-tools.js +12 -2
  27. package/dist/crawl/crawl.d.ts +13 -0
  28. package/dist/crawl/crawl.js +50 -2
  29. package/dist/daemon/daemon-resilience.js +2 -2
  30. package/dist/daemon/loopback-alias.d.ts +32 -0
  31. package/dist/daemon/loopback-alias.js +80 -0
  32. package/dist/events/contradictions.d.ts +14 -0
  33. package/dist/events/contradictions.js +26 -17
  34. package/dist/events/predicate-eval.d.ts +26 -3
  35. package/dist/events/predicate-eval.js +284 -95
  36. package/dist/events/predicate-parse.d.ts +1 -1
  37. package/dist/events/predicate-parse.js +18 -4
  38. package/dist/events/predicate.js +170 -18
  39. package/dist/events/ring-buffer.d.ts +8 -0
  40. package/dist/events/ring-buffer.js +34 -1
  41. package/dist/flows/flow-classify.js +1 -1
  42. package/dist/flows/flow-tools.js +12 -4
  43. package/dist/flows/heal-run.js +1 -1
  44. package/dist/honesty/blind-spots.js +6 -2
  45. package/dist/honesty/honesty.d.ts +11 -0
  46. package/dist/honesty/honesty.js +9 -1
  47. package/dist/honesty/unsettled.d.ts +40 -0
  48. package/dist/honesty/unsettled.js +78 -0
  49. package/dist/honesty/verified.d.ts +7 -0
  50. package/dist/honesty/verified.js +27 -4
  51. package/dist/http-server.js +11 -1
  52. package/dist/index.js +29 -2
  53. package/dist/init/agent-rules.d.ts +16 -0
  54. package/dist/init/agent-rules.js +96 -17
  55. package/dist/init/astro-patch.d.ts +2 -1
  56. package/dist/init/astro-patch.js +11 -5
  57. package/dist/init/capabilities.d.ts +22 -0
  58. package/dist/init/capabilities.js +76 -0
  59. package/dist/init/confirm.d.ts +96 -0
  60. package/dist/init/confirm.js +138 -0
  61. package/dist/init/cra.js +3 -2
  62. package/dist/init/detect.d.ts +8 -0
  63. package/dist/init/detect.js +15 -0
  64. package/dist/init/non-js-project.d.ts +27 -0
  65. package/dist/init/non-js-project.js +84 -0
  66. package/dist/init/plan-framework.d.ts +9 -0
  67. package/dist/init/plan-framework.js +72 -16
  68. package/dist/init/plan.d.ts +40 -4
  69. package/dist/init/plan.js +183 -43
  70. package/dist/init/run.d.ts +24 -2
  71. package/dist/init/run.js +181 -46
  72. package/dist/init/slash-command.d.ts +1 -1
  73. package/dist/init/slash-command.js +46 -6
  74. package/dist/init/snippets.d.ts +64 -5
  75. package/dist/init/snippets.js +220 -31
  76. package/dist/init/vite-config.js +29 -3
  77. package/dist/input/network-mock-tools.d.ts +19 -0
  78. package/dist/input/network-mock-tools.js +29 -4
  79. package/dist/license/license.js +1 -1
  80. package/dist/mcp/mcp-proxy.d.ts +69 -3
  81. package/dist/mcp/mcp-proxy.js +215 -23
  82. package/dist/mcp/mcp.js +58 -7
  83. package/dist/mcp/proxy-lifecycle.d.ts +20 -1
  84. package/dist/mcp/proxy-lifecycle.js +22 -1
  85. package/dist/pool/browser-pool.d.ts +22 -2
  86. package/dist/pool/browser-pool.js +44 -4
  87. package/dist/pool/playwright-launcher.js +10 -2
  88. package/dist/project/learned-routes.d.ts +24 -0
  89. package/dist/project/learned-routes.js +63 -0
  90. package/dist/project/project-store.d.ts +6 -0
  91. package/dist/project/project-store.js +25 -1
  92. package/dist/session/live-control-tools.js +39 -2
  93. package/dist/session/live-control.d.ts +7 -9
  94. package/dist/session/live-control.js +34 -1
  95. package/dist/session/no-session-diagnosis.d.ts +23 -4
  96. package/dist/session/no-session-diagnosis.js +128 -44
  97. package/dist/session/no-session-watch.js +12 -0
  98. package/dist/session/ref-provenance.d.ts +54 -0
  99. package/dist/session/ref-provenance.js +61 -0
  100. package/dist/session/review-store.d.ts +19 -0
  101. package/dist/session/review-store.js +34 -5
  102. package/dist/session/session-lease.js +1 -1
  103. package/dist/session/session-manager.d.ts +9 -0
  104. package/dist/session/session-manager.js +11 -0
  105. package/dist/session/session.d.ts +8 -0
  106. package/dist/session/session.js +12 -0
  107. package/dist/status-payload.d.ts +13 -1
  108. package/dist/status-payload.js +6 -1
  109. package/dist/telemetry/app-instrumented.d.ts +37 -0
  110. package/dist/telemetry/app-instrumented.js +54 -0
  111. package/dist/telemetry/automation-hint.d.ts +17 -0
  112. package/dist/telemetry/automation-hint.js +71 -0
  113. package/dist/telemetry/bug-found.d.ts +0 -20
  114. package/dist/telemetry/bug-found.js +38 -21
  115. package/dist/telemetry/cli-telemetry.js +7 -1
  116. package/dist/telemetry/daemon-telemetry.d.ts +4 -4
  117. package/dist/telemetry/daemon-telemetry.js +8 -4
  118. package/dist/telemetry/feedback.js +1 -1
  119. package/dist/telemetry/init-telemetry.js +9 -1
  120. package/dist/telemetry/install-source.d.ts +36 -0
  121. package/dist/telemetry/install-source.js +40 -0
  122. package/dist/telemetry/run-telemetry.js +4 -1
  123. package/dist/telemetry/session-metrics.d.ts +3 -3
  124. package/dist/telemetry/session-metrics.js +9 -4
  125. package/dist/telemetry/telemetry.d.ts +7 -1
  126. package/dist/telemetry/telemetry.js +17 -2
  127. package/dist/telemetry/tool-refused.d.ts +19 -0
  128. package/dist/telemetry/tool-refused.js +55 -0
  129. package/dist/telemetry/verification-of.js +72 -8
  130. package/dist/temp-dir.d.ts +27 -0
  131. package/dist/temp-dir.js +46 -0
  132. package/dist/tools/act-tools.js +77 -11
  133. package/dist/tools/assert-grade.d.ts +1 -1
  134. package/dist/tools/assert-grade.js +27 -5
  135. package/dist/tools/assert-verdict.js +25 -1
  136. package/dist/tools/contract-tools.js +4 -0
  137. package/dist/tools/dynamic-tools.js +1 -1
  138. package/dist/tools/error-recovery.d.ts +10 -0
  139. package/dist/tools/error-recovery.js +70 -5
  140. package/dist/tools/feedback-invite.js +1 -1
  141. package/dist/tools/invoke-tool.js +149 -32
  142. package/dist/tools/lease-tools.d.ts +29 -1
  143. package/dist/tools/lease-tools.js +58 -5
  144. package/dist/tools/navigate-result.js +1 -1
  145. package/dist/tools/observe-tools.js +1 -1
  146. package/dist/tools/observed-nothing.js +1 -1
  147. package/dist/tools/react-grace.d.ts +48 -0
  148. package/dist/tools/react-grace.js +87 -0
  149. package/dist/tools/read-tools.js +20 -4
  150. package/dist/tools/settle-in-flight.d.ts +79 -0
  151. package/dist/tools/settle-in-flight.js +106 -0
  152. package/dist/tools/snapshot-delta.d.ts +7 -0
  153. package/dist/tools/snapshot-delta.js +130 -6
  154. package/dist/tools/tool-kit.d.ts +52 -5
  155. package/dist/tools/tool-kit.js +55 -24
  156. package/dist/tools/tool-surface.js +28 -9
  157. package/dist/tools/tools.js +38 -4
  158. package/dist/visual/capture-cleanup.d.ts +16 -0
  159. package/dist/visual/capture-cleanup.js +39 -0
  160. package/dist/visual/visual-tools.d.ts +18 -0
  161. package/dist/visual/visual-tools.js +24 -5
  162. package/docs/README.md +56 -11
  163. package/docs/actions.mdx +87 -0
  164. package/docs/agent-cheatsheet.md +102 -26
  165. package/docs/architecture.md +46 -35
  166. package/docs/benchmarks.md +76 -72
  167. package/docs/best-practices.mdx +211 -0
  168. package/docs/capabilities.mdx +387 -0
  169. package/docs/cli/affected.mdx +51 -0
  170. package/docs/cli/capsules.mdx +69 -0
  171. package/docs/cli/cloud.mdx +127 -0
  172. package/docs/cli/doctor.mdx +76 -0
  173. package/docs/cli/drive.mdx +55 -0
  174. package/docs/cli/feedback.mdx +67 -0
  175. package/docs/cli/gate.mdx +64 -0
  176. package/docs/cli/help.mdx +92 -0
  177. package/docs/cli/hunt.mdx +57 -0
  178. package/docs/cli/identify.mdx +62 -0
  179. package/docs/cli/init.mdx +94 -0
  180. package/docs/cli/kill.mdx +91 -0
  181. package/docs/cli/license.mdx +48 -0
  182. package/docs/cli/mcp.mdx +61 -0
  183. package/docs/cli/open.mdx +68 -0
  184. package/docs/cli/restart.mdx +60 -0
  185. package/docs/cli/rollback.mdx +35 -0
  186. package/docs/cli/serve.mdx +62 -0
  187. package/docs/cli/status.mdx +64 -0
  188. package/docs/cli/stop.mdx +56 -0
  189. package/docs/cli/telemetry.mdx +57 -0
  190. package/docs/cli/update.mdx +52 -0
  191. package/docs/cli/verify.mdx +57 -0
  192. package/docs/cli/version.mdx +39 -0
  193. package/docs/cli/watch.mdx +55 -0
  194. package/docs/cli.mdx +219 -0
  195. package/docs/debugging.md +13 -9
  196. package/docs/deploy-checks.md +15 -11
  197. package/docs/desktop-apps.md +49 -40
  198. package/docs/desktop.mdx +188 -0
  199. package/docs/docs.json +363 -0
  200. package/docs/enterprise.md +20 -16
  201. package/docs/faq.mdx +88 -0
  202. package/docs/fixtures.md +11 -7
  203. package/docs/flows.md +26 -22
  204. package/docs/for-agents.md +71 -0
  205. package/docs/frameworks.mdx +199 -0
  206. package/docs/gate-plan.md +45 -41
  207. package/docs/gates.md +23 -19
  208. package/docs/getting-started.md +76 -65
  209. package/docs/human-control.md +22 -18
  210. package/docs/index.mdx +101 -0
  211. package/docs/install-agentic.mdx +215 -0
  212. package/docs/install-manual.mdx +194 -0
  213. package/docs/instrumentation.mdx +232 -0
  214. package/docs/integration-patterns.md +38 -34
  215. package/docs/local-registry.md +16 -12
  216. package/docs/multi-agent-testing.md +16 -12
  217. package/docs/packages/babel-plugin.mdx +70 -0
  218. package/docs/packages/browser.mdx +111 -0
  219. package/docs/packages/core.mdx +106 -0
  220. package/docs/packages/electron.mdx +87 -0
  221. package/docs/packages/eslint-plugin.mdx +84 -0
  222. package/docs/packages/next.mdx +91 -0
  223. package/docs/packages/react.mdx +91 -0
  224. package/docs/packages/server.mdx +98 -0
  225. package/docs/packages/tauri.mdx +63 -0
  226. package/docs/packages/test.mdx +108 -0
  227. package/docs/packages/vite-plugin.mdx +96 -0
  228. package/docs/packages.mdx +217 -0
  229. package/docs/platform-integration.md +33 -26
  230. package/docs/predicates.mdx +179 -0
  231. package/docs/quickstart.mdx +211 -0
  232. package/docs/recipes.mdx +242 -0
  233. package/docs/skill-file.mdx +105 -0
  234. package/docs/state-management.mdx +197 -0
  235. package/docs/style.css +14 -0
  236. package/docs/system-map.md +47 -32
  237. package/docs/telemetry-contract.md +101 -46
  238. package/docs/telemetry-events.mdx +430 -0
  239. package/docs/telemetry.md +63 -50
  240. package/docs/testing.md +10 -6
  241. package/docs/token-efficiency.md +30 -26
  242. package/docs/tools-act-and-wait.mdx +123 -0
  243. package/docs/tools-act-sequence.mdx +120 -0
  244. package/docs/tools-act.mdx +82 -0
  245. package/docs/tools-assert.mdx +70 -0
  246. package/docs/tools-console.mdx +59 -0
  247. package/docs/tools-inspect.mdx +85 -0
  248. package/docs/tools-navigate.mdx +63 -0
  249. package/docs/tools-network.mdx +86 -0
  250. package/docs/tools-observe.mdx +89 -0
  251. package/docs/tools-overview.mdx +157 -0
  252. package/docs/tools-query.mdx +82 -0
  253. package/docs/tools-session-and-feedback.mdx +91 -0
  254. package/docs/tools-sessions.mdx +65 -0
  255. package/docs/tools-snapshot.mdx +75 -0
  256. package/docs/tools-state.mdx +81 -0
  257. package/docs/tools-tools-and-run.mdx +125 -0
  258. package/docs/tools-wait-for.mdx +66 -0
  259. package/docs/troubleshooting.mdx +193 -0
  260. package/docs/usage.md +175 -153
  261. package/docs/vs-chrome-devtools-mcp.mdx +82 -0
  262. package/docs/vs-playwright-mcp.mdx +85 -0
  263. package/docs/vs-screenshots.mdx +96 -0
  264. package/docs/what-is-reticle.mdx +63 -0
  265. package/docs/why-reticle.mdx +107 -0
  266. package/package.json +2 -2
  267. package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
  268. package/docs/matrix/2.5.0/codex-machine.json +0 -20
  269. package/docs/matrix/2.5.0/cursor-machine.json +0 -22
  270. package/docs/matrix/2.5.0/gemini-machine.json +0 -22
  271. package/docs/matrix/2.5.0/opencode-machine.json +0 -22
  272. package/docs/matrix/2.5.0/vscode-machine.json +0 -22
  273. package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
  274. package/docs/matrix/MATRIX.md +0 -26
  275. package/docs/matrix/README.md +0 -59
  276. package/docs/mint.json +0 -61
@@ -14,32 +14,63 @@ export const sessionIdShape = {
14
14
  .describe('OMIT THIS unless you mean a specific tab — Reticle scopes to your project, prefers the active one, and refuses rather than guesses when ambiguous. Pass an id from reticle_sessions only to target a particular tab. The previous wording ("omit when only ONE session is open") was wrong and cost real turns: with three sessions connected — an app plus two pool leases — resolution is still unambiguous, but that sentence sent the agent off to list and filter sessions by hand before every single call.'),
15
15
  };
16
16
  /**
17
- * Fields that `runTool` / `withControl` splice onto EVERY session-bound tool result at runtime
18
- * (health, pool lease reminder, age cleanup nudge, and the delivered-once human-control envelope).
19
- * They are declared here and merged into each session-bound tool's outputSchema so a schema-strict
20
- * client (structuredContent validation) keeps them instead of silently dropping them — the `control`
21
- * envelope is the human-in-the-loop guidance channel, so losing it is a safety failure, not cosmetic.
17
+ * Every key `runTool` splices onto a session-bound result, as a closed list.
18
+ *
19
+ * These are wire strings and were inlined at the splice sites, which is exactly how four of them
20
+ * came to be spliced and never declared. `sessionEnvelopeShape` is derived from this object and
21
+ * `envelope-keys-declared.test.ts` fails if a member is missing from it, so the two cannot drift.
22
+ *
23
+ * Each of these is a channel that exists BECAUSE the agent had no other way to learn the fact.
24
+ * Undeclared, a schema-strict client validating `structuredContent` drops it, and the channel is
25
+ * indistinguishable from never having been built.
22
26
  */
23
- export const sessionEnvelopeShape = {
24
- session: z.unknown().optional(),
25
- session_lease: z.unknown().optional(),
26
- session_age_warning: z.unknown().optional(),
27
- control: z.unknown().optional(),
28
- // `warning` rides alongside `session` whenever the tab is THROTTLED (healthEnvelope splices both).
29
- // It was declared on reticle_act's schema but nowhere else, so on a validating profile every other
30
- // session-bound tool (observe, assert, wait_for, act_sequence, act_and_wait, snapshot, query, …)
31
- // silently dropped it — a throttled tab, where drives can no-op, returned a healthy-looking result.
32
- // That is exactly what invoke-tool.ts's health splice exists to prevent, so it belongs in the shared
33
- // envelope, not per-tool.
34
- warning: z.string().optional(),
35
- // The one-shot "ask the human how this went" envelope. Undeclared, a validating profile would strip
36
- // it — and a feedback prompt that never reaches the agent is a feedback prompt that never reaches
37
- // the person, which is the entire failure mode this channel exists to fix.
38
- feedback_prompt: z.unknown().optional(),
39
- // One-shot "a newer Reticle exists" notice. Undeclared, a validating profile would strip it and the
40
- // agent would never learn an update was waiting.
41
- update_available: z.unknown().optional(),
27
+ export const EnvelopeKey = {
28
+ /** Session health — which tab, how fresh. */
29
+ SESSION: 'session',
30
+ /** One-shot pool-lease reminder. */
31
+ SESSION_LEASE: 'session_lease',
32
+ /** One-shot "these sessions are old, clean them up". */
33
+ SESSION_AGE_WARNING: 'session_age_warning',
34
+ /** The delivered-once human-in-the-loop guidance envelope. Losing it is a safety failure. */
35
+ CONTROL: 'control',
36
+ /**
37
+ * Rides alongside `session` whenever the tab is THROTTLED. It was declared on `reticle_act`'s
38
+ * schema and nowhere else, so on a validating profile every other session-bound tool silently
39
+ * dropped it — a throttled tab, where drives can no-op, returned a healthy-looking result.
40
+ */
41
+ WARNING: 'warning',
42
+ /** One-shot "ask the human how this went". */
43
+ FEEDBACK_PROMPT: 'feedback_prompt',
44
+ /** One-shot "a newer Reticle exists". */
45
+ UPDATE_AVAILABLE: 'update_available',
46
+ /**
47
+ * "You have driven this page N times without asking for a verdict."
48
+ *
49
+ * The largest known lever on the metric this product is judged by: verdict-less sessions
50
+ * overwhelmingly never call a verdict-producing tool at all. It was spliced undeclared, so on a
51
+ * validating client the nudge was built, fired, and thrown away before any agent saw it.
52
+ */
53
+ VERIFY_NEXT: 'verify_next',
54
+ /** The contextual "tell us what just went wrong" invitation. Same story, same release. */
55
+ FEEDBACK_INVITE: 'feedback_invite',
56
+ /** SDK/daemon version skew — often the one fact that explains everything else in the session. */
57
+ VERSION_SKEW: 'version_skew',
58
+ /** A feedback report that was accepted and then failed to send. Only the reporter can act on it. */
59
+ FEEDBACK_UNDELIVERED: 'feedback_undelivered',
42
60
  };
61
+ /**
62
+ * Fields that `runTool` / `withControl` splice onto EVERY session-bound tool result at runtime.
63
+ * Merged into each session-bound tool's outputSchema so a schema-strict client (structuredContent
64
+ * validation) keeps them instead of silently dropping them.
65
+ *
66
+ * Derived from `EnvelopeKey` rather than re-listed: this shape and the splice sites disagreed for a
67
+ * whole release, and every key that went missing was a channel nobody could see was missing.
68
+ */
69
+ export const sessionEnvelopeShape = Object.fromEntries(Object.values(EnvelopeKey).map((key) => [
70
+ key,
71
+ // `warning` is the one with a narrower type; the rest are opaque envelopes the client forwards.
72
+ EnvelopeKey.WARNING === key ? z.string().optional() : z.unknown().optional(),
73
+ ]));
43
74
  /** Unwrap a browser command result or throw its error so the agent sees a clean failure. */
44
75
  export async function commandOrThrow(deps, sessionId,
45
76
  // ReticleCommand, not string: the union already exists and every caller passes a member. Typed as
@@ -100,16 +100,16 @@ export const CORE_TOOL_NAMES = new Set([
100
100
  ReticleTool.ACT_AND_WAIT,
101
101
  // ACT_SEQUENCE is here because its absence was measurably CAUSING the biggest loop in the data.
102
102
  //
103
- // Measured 2026-08-10/11 (non-CI): `reticle_act` 319 calls, `reticle_act_sequence` **7**. `act`
104
- // also leads the repeat table at 110 consecutive-repeat runs, and inside those sessions the
105
- // repeated calls are 98 clicks and 21 fills — a login form driven one round trip at a time, which
106
- // is exactly the antipattern SKILL.md warns about and exactly what this tool exists to collapse.
103
+ // In the field `reticle_act` is called overwhelmingly more often than `reticle_act_sequence`, and
104
+ // `act` leads the repeat table by a wide margin — inside those sessions the repeated calls are
105
+ // clicks and fills, a login form driven one round trip at a time, which is exactly the antipattern
106
+ // SKILL.md warns about and exactly what this tool exists to collapse.
107
107
  //
108
- // The repeats are NOT retries: looping sessions have a lower error rate (0.051) than non-looping
109
- // ones (0.078). The calls succeed and get repeated, because the batching tool was reachable only
110
- // through `reticle_run` — so an agent had to already know it existed to use it, and 7-vs-319 says
111
- // essentially nobody did. A tool an agent must already know about is a tool that never gets
112
- // called; the same argument that put INSPECT and FEEDBACK here.
108
+ // The repeats are NOT retries: looping sessions have a LOWER error rate than non-looping ones. The
109
+ // calls succeed and get repeated, because the batching tool was reachable only through
110
+ // `reticle_run` — so an agent had to already know it existed to use it, and essentially nobody
111
+ // did. A tool an agent must already know about is a tool that never gets called; the same argument
112
+ // that put INSPECT and FEEDBACK here.
113
113
  ReticleTool.ACT_SEQUENCE,
114
114
  ReticleTool.OBSERVE,
115
115
  ReticleTool.NETWORK,
@@ -144,6 +144,25 @@ export const CORE_TOOL_NAMES = new Set([
144
144
  // deleting the instruction from the lease and the pause hint — deletes the handback protocol,
145
145
  // because there is nowhere else those two calls are ever named.
146
146
  ReticleTool.SESSION,
147
+ // CAPABILITIES is the highest orientation-per-token tool on the surface: one call returns the app's
148
+ // whole testable surface — its testids, signals, stores and named flows — and `{fromDisk:true}`
149
+ // returns the same from a checked-in contract with NO browser attached, so an agent with zero
150
+ // context starts oriented instead of exploring.
151
+ //
152
+ // Same argument as the four above, and it is the fifth time this file has made it: a tool an agent
153
+ // must already know about, and reach through reticle_run, is a tool that never gets called.
154
+ //
155
+ // The cost is real and is NOT the bargain #262 claimed. That issue argued the marginal cost sits
156
+ // "well below the 274-token average" because the input schema is small; measured, its description
157
+ // plus schema is closer to 325 tokens, which is above the average rather than below it. This is
158
+ // also a straight +1 rather than a swap: the tools that write-up proposed demoting (`project`,
159
+ // `domain`) were never in the default set to begin with.
160
+ //
161
+ // So it is a deliberate bet, taken with that correction on the table: that orientation replaces
162
+ // exploratory snapshots, which are among the most expensive calls an agent makes. The measurement
163
+ // that settles it is whether sessions calling this early take fewer and cheaper snapshots than
164
+ // those that do not. If that does not show up, this is the entry to revert.
165
+ ReticleTool.CAPABILITIES,
147
166
  ]);
148
167
  /** Is the truthy form of a boolean env var set? `1`, `true`, `yes` — anything else is off. */
149
168
  function envFlagOn(raw) {
@@ -78,6 +78,10 @@ const RAW_TOOLS = [
78
78
  recommendation: z.string().optional(),
79
79
  }))
80
80
  .describe('Connected browser sessions with health state. `projectId` groups sessions by app (stable across port changes); `leased` marks a pool-managed headless context vs a human tab.'),
81
+ why: z
82
+ .string()
83
+ .optional()
84
+ .describe('Present ONLY when `sessions` is empty: why nothing is connected, and the next action that fixes it. An empty list is never the end of the road — read this before concluding the app cannot be driven.'),
81
85
  },
82
86
  handler: async (deps) => {
83
87
  const provider = deps.realInput;
@@ -87,6 +91,14 @@ const RAW_TOOLS = [
87
91
  realInputAvailable: provider !== undefined ? await provider.isAvailableFor(s.url) : false,
88
92
  leased: leasedIds.has(s.sessionId),
89
93
  })));
94
+ // An empty list is the most common thing this tool ever returns, and on its own it is a dead
95
+ // end: the agent asked the one question it knows to ask, got a confident-looking answer with
96
+ // no next step, and stopped. The daemon already knows WHY nothing is connected — say it here
97
+ // rather than only when a later tool fails for want of a session.
98
+ if (0 === sessions.length) {
99
+ const why = deps.sessions.noSessionHint();
100
+ return { sessions, ...(why === undefined ? {} : { why }) };
101
+ }
90
102
  return { sessions };
91
103
  },
92
104
  },
@@ -193,6 +205,10 @@ const RAW_TOOLS = [
193
205
  .string()
194
206
  .optional()
195
207
  .describe('CSS selector or element ref to restrict the search to a subtree.'),
208
+ self: z
209
+ .boolean()
210
+ .optional()
211
+ .describe('Return the `scope` element ITSELF instead of searching inside it. Use when the target is a plain layout container with no role, name, testid or text of its own — which is routinely the element that carries the handler, and is otherwise unreachable because every query excludes its own scope root. Requires `scope`.'),
196
212
  attrs: z
197
213
  .array(z.string())
198
214
  .optional()
@@ -297,7 +313,13 @@ const RAW_TOOLS = [
297
313
  // The handler forwards an explicit allowlist, so a new input is silently dropped unless it is
298
314
  // added here — the browser saw no `attrs` and returned undefined while the unit tests, which
299
315
  // call matchQuery directly, passed. Schema plus implementation is not the whole wire.
316
+ //
317
+ // It happened again with `self`, in the same session that wrote this comment: declared on the
318
+ // tool, implemented in the browser, verified by unit tests that call `runQuery` directly, and
319
+ // dropped here — so the live call returned zero matches with no error. `query-forwarding.test.ts`
320
+ // now asserts the payload, because a comment is not a guard.
300
321
  attrs: args['attrs'],
322
+ self: args['self'],
301
323
  }).then((result) => withSizeCost(paginateQueryResult(result, asNumber(args['limit']), true === args['count_only'])));
302
324
  },
303
325
  },
@@ -336,10 +358,22 @@ const RAW_TOOLS = [
336
358
  })
337
359
  .partial()
338
360
  .optional(),
339
- // Theme compliance vs the app's design tokens: { colorToken, backgroundToken (null = off-palette),
340
- // offTheme, tokenCount }. Kept as unknown — the structured-content serializer can truncate a
341
- // large inspect payload's fields to strings, which a strict shape would reject; the full object
342
- // is always present in the text content the agent reads.
361
+ scroll: z
362
+ .object({
363
+ scrollTop: z.number(),
364
+ scrollHeight: z.number(),
365
+ clientHeight: z.number(),
366
+ overflowY: z.string(),
367
+ })
368
+ .optional(),
369
+ // Theme compliance vs the app's design tokens: { colorToken, colorTokens, backgroundToken,
370
+ // backgroundTokens, offTheme, tokenCount, themeScope }. The plural fields carry EVERY token
371
+ // matching the resolved colour; the singular ones abstain (null) when several tokens share it,
372
+ // because returning an arbitrary winner was the defect (#313). `themeScope` names the theme
373
+ // that was active at capture, so two inspects minutes apart are comparable at all.
374
+ // Kept as unknown — the structured-content serializer can truncate a large inspect payload's
375
+ // fields to strings, which a strict shape would reject; the full object is always present in
376
+ // the text content the agent reads.
343
377
  theme: z.unknown().optional(),
344
378
  /**
345
379
  * Where this element is written, as `file:line`. Present when the app is built with the
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Remember the private parent of a capture after the daemon has read it.
3
+ *
4
+ * Legacy captures live directly in `tmpdir()` and therefore have no directory to reclaim. The
5
+ * caller has already applied the capture-path trust-boundary check; this second exact parent check
6
+ * keeps the cleanup set limited to the one-level private layout.
7
+ */
8
+ export declare function trackCaptureDirectory(path: string): void;
9
+ /**
10
+ * Remove empty private capture directories during the daemon's existing awaited shutdown path.
11
+ *
12
+ * Deliberately `rmdir`, never recursive removal: captures are unlinked after reading, so a genuine
13
+ * directory is empty by shutdown. If anything unexpected remains, refusing to remove the directory
14
+ * is safer than widening a renderer-supplied path into a recursive delete.
15
+ */
16
+ export declare function cleanupCaptureDirectories(): Promise<void>;
@@ -0,0 +1,39 @@
1
+ import { rmdir } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { basename, dirname } from 'node:path';
4
+ import { RETICLE_CAPTURE_FILE_PREFIX } from '@reticlehq/core';
5
+ /** Private shell directories whose captures this daemon has successfully consumed. */
6
+ const captureDirectories = new Set();
7
+ /**
8
+ * Remember the private parent of a capture after the daemon has read it.
9
+ *
10
+ * Legacy captures live directly in `tmpdir()` and therefore have no directory to reclaim. The
11
+ * caller has already applied the capture-path trust-boundary check; this second exact parent check
12
+ * keeps the cleanup set limited to the one-level private layout.
13
+ */
14
+ export function trackCaptureDirectory(path) {
15
+ const parent = dirname(path);
16
+ if (dirname(parent) === tmpdir() && basename(parent).startsWith(RETICLE_CAPTURE_FILE_PREFIX)) {
17
+ captureDirectories.add(parent);
18
+ }
19
+ }
20
+ /**
21
+ * Remove empty private capture directories during the daemon's existing awaited shutdown path.
22
+ *
23
+ * Deliberately `rmdir`, never recursive removal: captures are unlinked after reading, so a genuine
24
+ * directory is empty by shutdown. If anything unexpected remains, refusing to remove the directory
25
+ * is safer than widening a renderer-supplied path into a recursive delete.
26
+ */
27
+ export async function cleanupCaptureDirectories() {
28
+ await Promise.all([...captureDirectories].map(async (dir) => {
29
+ try {
30
+ await rmdir(dir);
31
+ }
32
+ catch {
33
+ // Best-effort shutdown housekeeping. A non-empty or already-removed directory is harmless.
34
+ }
35
+ finally {
36
+ captureDirectories.delete(dir);
37
+ }
38
+ }));
39
+ }
@@ -1,4 +1,22 @@
1
1
  import type { ToolDef } from '../tools/tools.js';
2
+ /**
3
+ * Only read a capture the shell wrote. The path arrives from the PAGE, and the daemon must not
4
+ * become a file-read oracle for whatever a compromised renderer names.
5
+ *
6
+ * Two layouts are accepted, and the pair is the whole subtlety:
7
+ *
8
+ * - `<tmp>/<prefix><dir>/<prefix><n>.png` — the shell's own PRIVATE capture directory, created 0700
9
+ * so no other local user can read the screenshot or pre-place a symlink at the path we write.
10
+ * - `<tmp>/<prefix><pid>-<n>.png` — the legacy flat layout. Kept because the shell ships as a
11
+ * SEPARATE package on the user's own upgrade schedule: an app still on the older
12
+ * `@reticlehq/electron` talking to a newer daemon must keep getting screenshots, not silently
13
+ * start returning no image. It grants the daemon nothing it did not already have.
14
+ *
15
+ * Every comparison is an exact match against an UNRESOLVED path, which is what refuses traversal:
16
+ * `<tmp>/<prefix>x/../../etc/<prefix>passwd.png` has a grandparent of `<tmp>/<prefix>x/..`, not
17
+ * `<tmp>`. Exactly one directory level is allowed, so a deeper nesting is refused too.
18
+ */
19
+ export declare function isCapturePath(path: string): boolean;
2
20
  /**
3
21
  * The opt-in pixel layer. Both tools require a DRIVEN browser (reticle drive /
4
22
  * RETICLE_CDP_URL) — the always-on SDK ships no screenshotter — and return a structured NO_PROVIDER
@@ -5,6 +5,7 @@ import { sessionIdShape } from '../tools/tool-kit.js';
5
5
  import { asNumber, asRecord, asString } from '../tools/tools-helpers.js';
6
6
  import { diffPng } from './visual-diff.js';
7
7
  import { VisualStore } from './visual-store.js';
8
+ import { trackCaptureDirectory } from './capture-cleanup.js';
8
9
  import { readFile, unlink } from 'node:fs/promises';
9
10
  import { tmpdir } from 'node:os';
10
11
  import { basename, dirname } from 'node:path';
@@ -87,6 +88,7 @@ async function desktopCapture(deps, sessionId, fullPage) {
87
88
  try {
88
89
  const bytes = await readFile(path);
89
90
  await unlink(path).catch(() => undefined);
91
+ trackCaptureDirectory(path);
90
92
  return isCompletePng(bytes) ? { png: new Uint8Array(bytes) } : {};
91
93
  }
92
94
  catch {
@@ -94,12 +96,29 @@ async function desktopCapture(deps, sessionId, fullPage) {
94
96
  }
95
97
  }
96
98
  /**
97
- * Only read a capture the shell wrote: inside the OS temp dir, with Reticle's own filename prefix.
98
- * The path arrives from the PAGE, and the daemon must not become a file-read oracle for whatever a
99
- * compromised renderer names.
99
+ * Only read a capture the shell wrote. The path arrives from the PAGE, and the daemon must not
100
+ * become a file-read oracle for whatever a compromised renderer names.
101
+ *
102
+ * Two layouts are accepted, and the pair is the whole subtlety:
103
+ *
104
+ * - `<tmp>/<prefix><dir>/<prefix><n>.png` — the shell's own PRIVATE capture directory, created 0700
105
+ * so no other local user can read the screenshot or pre-place a symlink at the path we write.
106
+ * - `<tmp>/<prefix><pid>-<n>.png` — the legacy flat layout. Kept because the shell ships as a
107
+ * SEPARATE package on the user's own upgrade schedule: an app still on the older
108
+ * `@reticlehq/electron` talking to a newer daemon must keep getting screenshots, not silently
109
+ * start returning no image. It grants the daemon nothing it did not already have.
110
+ *
111
+ * Every comparison is an exact match against an UNRESOLVED path, which is what refuses traversal:
112
+ * `<tmp>/<prefix>x/../../etc/<prefix>passwd.png` has a grandparent of `<tmp>/<prefix>x/..`, not
113
+ * `<tmp>`. Exactly one directory level is allowed, so a deeper nesting is refused too.
100
114
  */
101
- function isCapturePath(path) {
102
- return dirname(path) === tmpdir() && basename(path).startsWith(RETICLE_CAPTURE_FILE_PREFIX);
115
+ export function isCapturePath(path) {
116
+ if (!basename(path).startsWith(RETICLE_CAPTURE_FILE_PREFIX))
117
+ return false;
118
+ const parent = dirname(path);
119
+ if (parent === tmpdir())
120
+ return true;
121
+ return dirname(parent) === tmpdir() && basename(parent).startsWith(RETICLE_CAPTURE_FILE_PREFIX);
103
122
  }
104
123
  /**
105
124
  * A PNG is complete only if it ends with IEND. This check exists because a truncated image once
package/docs/README.md CHANGED
@@ -1,31 +1,73 @@
1
1
  # Docs index
2
2
 
3
- Two audiences share this directory, which is why it looked like twenty-five unsorted files. Every page states its own audience in the first blockquote; this index is the shortcut.
3
+ Two audiences share this directory: people using Reticle and people working on it. Every page carries a `description` in its frontmatter saying what it answers; this index is the shortcut when you want to scan all of them at once.
4
4
 
5
- **User docs are published** to [reticle.sh](https://reticle.sh) via `mint.json`. **Contributor docs are not** — they are for people working _on_ Reticle, and they live here because they belong next to what they describe.
5
+ Everything here is published to [docs.reticle.sh](https://docs.reticle.sh) by `docs.json`, which is the Mintlify config for this directory. Five tabs. **Guides** to learn in order, grouped by what you are doing (get started, install, instrument, verify and ship, work with agents, and when it goes wrong). **Reference** to look one thing up. **Desktop** because Electron and Tauri are a different shape of problem and deserve their own front door, **Resources** to decide or to check a claim, and **Contributing** for people working on Reticle. Contributor docs sit in a tab rather than hidden, since they were reachable by URL anyway, and an unlisted page is a page nobody finds.
6
6
 
7
7
  ---
8
8
 
9
9
  ## For people using Reticle
10
10
 
11
- **Start:** [getting-started.md](getting-started.md) → [usage.md](usage.md) (the full tool reference).
11
+ **Start:** [quickstart.mdx](quickstart.mdx) → [usage.md](usage.md) (the full tool reference).
12
12
 
13
13
  | Page | What it answers |
14
14
  | --- | --- |
15
+ | [quickstart.mdx](quickstart.mdx) | five minutes to a real verdict; every response on it was captured live |
16
+ | [what-is-reticle.mdx](what-is-reticle.mdx) | the definition, what Reticle is NOT, and what it needs to run |
17
+ | [why-reticle.mdx](why-reticle.mdx) | the false-green problem, the measured case, and where we lose |
18
+ | [install-agentic.mdx](install-agentic.mdx) | what `npx @reticlehq/server init` writes, and how to read its marks |
19
+ | [install-manual.mdx](install-manual.mdx) | wiring the MCP server and SDK by hand, per agent and framework |
15
20
  | [getting-started.md](getting-started.md) | install it, connect an agent, verify something |
16
- | [usage.md](usage.md) | every tool, every argument — the reference |
21
+ | [usage.md](usage.md) | the long-form narrative; most of it now has a focused page, and it says so at the top |
22
+ | [predicates.mdx](predicates.mdx) | the predicate grammar, and which kinds actually prove something |
23
+ | [capabilities.mdx](capabilities.mdx) | coverage, storage, fake clocks, crawl, visual diff, network mocking |
24
+ | [recipes.mdx](recipes.mdx) | worked examples for real situations, with the response each returned |
25
+ | [actions.mdx](actions.mdx) | every action and its arguments, including `press` and its history |
26
+ | [faq.mdx](faq.mdx) | production, frameworks, comparisons, and the honest limits |
27
+ | [tools-overview.mdx](tools-overview.mdx) | the 18 advertised tools, the 30 in the cold tail, and why |
28
+ | [tools-snapshot.mdx](tools-snapshot.mdx) | `reticle_snapshot`: three modes, from full tree to a 25-token route check |
29
+ | [tools-query.mdx](tools-query.mdx) | `reticle_query` finds elements by testing-library semantics |
30
+ | [tools-inspect.mdx](tools-inspect.mdx) | `reticle_inspect`: one element, down to source and design tokens |
31
+ | [tools-navigate.mdx](tools-navigate.mdx) | `reticle_navigate`, and why `ok` does not mean the page arrived |
32
+ | [tools-act.mdx](tools-act.mdx) | `reticle_act` acts, proves nothing, and says so |
33
+ | [tools-act-and-wait.mdx](tools-act-and-wait.mdx) | `reticle_act_and_wait`, the tool that produces a verdict |
34
+ | [tools-act-sequence.mdx](tools-act-sequence.mdx) | `reticle_act_sequence`: batch a form into one round trip |
35
+ | [tools-observe.mdx](tools-observe.mdx) | `reticle_observe`, the whole timeline when you don't know what broke |
36
+ | [tools-network.mdx](tools-network.mdx) | `reticle_network`: the request log, redaction, and buffer honesty |
37
+ | [tools-console.mdx](tools-console.mdx) | `reticle_console`, and an empty result that proves it looked |
38
+ | [tools-state.mdx](tools-state.mdx) | `reticle_state`: what the app believes, not what it drew |
39
+ | [tools-wait-for.mdx](tools-wait-for.mdx) | `reticle_wait_for`, for consequences you did not cause |
40
+ | [tools-assert.mdx](tools-assert.mdx) | `reticle_assert`: verdicts, and a real failure explained |
41
+ | [tools-sessions.mdx](tools-sessions.mdx) | `reticle_sessions`: the health fields that decide if driving works |
42
+ | [tools-tools-and-run.mdx](tools-tools-and-run.mdx) | `reticle_tools` / `reticle_run`, reaching the other 30 |
43
+ | [tools-session-and-feedback.mdx](tools-session-and-feedback.mdx) | `reticle_session` / `reticle_feedback`, the human boundary |
44
+ | [packages.mdx](packages.mdx) | every library at a glance: what it does, why it exists, when you need it |
45
+ | [packages/](packages/) | one page per published package, with its real exported API and dependencies |
46
+ | [best-practices.mdx](best-practices.mdx) | the habits that make a verdict worth trusting |
47
+ | [skill-file.mdx](skill-file.mdx) | the paste-one-URL skill that teaches an agent Reticle |
17
48
  | [agent-cheatsheet.md](agent-cheatsheet.md) | the condensed version an agent keeps in context |
49
+ | [for-agents.md](for-agents.md) | how to fetch these docs as Markdown or `llms.txt` |
18
50
  | [architecture.md](architecture.md) | how it works, and why it is built this way |
19
51
  | [platform-integration.md](platform-integration.md) | Vite, Next, Remix, Astro, plain HTML |
20
- | [desktop-apps.md](desktop-apps.md) | Electron and Tauri |
52
+ | [desktop.mdx](desktop.mdx) | why desktop is different, and what only Reticle does there |
53
+ | [desktop-apps.md](desktop-apps.md) | Electron and Tauri, in full |
54
+ | [frameworks.mdx](frameworks.mdx) | what is supported, what is wired-but-unverified, and the wiring |
55
+ | [state-management.mdx](state-management.mdx) | zustand, Redux, and eight adapters |
56
+ | [instrumentation.mdx](instrumentation.mdx) | stores, signals, testids, and how verdicts get stronger |
57
+ | [troubleshooting.mdx](troubleshooting.mdx) | nothing connected, click did nothing, verdict says unknown |
58
+ | [cli.mdx](cli.mdx) | the CLI at a glance, with real output and measured exit codes |
59
+ | [cli/](cli/) | one page per command: every flag, what it prints, and its exit codes |
21
60
  | [integration-patterns.md](integration-patterns.md) | wiring it into a real codebase |
22
61
  | [flows.md](flows.md) | record → save → replay → heal |
23
- | [testing.md](testing.md) | `@reticlehq/test` — turning a session into a CI suite |
62
+ | [testing.md](testing.md) | `@reticlehq/test`, turning a session into a CI suite |
24
63
  | [multi-agent-testing.md](multi-agent-testing.md) | more than one agent on one app |
25
64
  | [human-control.md](human-control.md) | taking the wheel back from the agent |
26
65
  | [deploy-checks.md](deploy-checks.md) | running Reticle against a deployed build |
27
66
  | [token-efficiency.md](token-efficiency.md) | why it costs less than a screenshot loop |
28
67
  | [benchmarks.md](benchmarks.md) | how the numbers were measured, including where Reticle loses |
68
+ | [vs-playwright-mcp.mdx](vs-playwright-mcp.mdx) | outside-in vs inside-out, and when Playwright is right |
69
+ | [vs-chrome-devtools-mcp.mdx](vs-chrome-devtools-mcp.mdx) | cheaper per look, catches less: the trade, measured |
70
+ | [vs-screenshots.mdx](vs-screenshots.mdx) | why a better vision model does not fix a non-visual bug |
29
71
  | [telemetry.md](telemetry.md) | what is collected, and how to turn it off |
30
72
  | [local-registry.md](local-registry.md) | installing an unpublished build |
31
73
  | [enterprise.md](enterprise.md) | the licensed surface |
@@ -36,20 +78,23 @@ Two audiences share this directory, which is why it looked like twenty-five unso
36
78
 
37
79
  | Page | What it answers |
38
80
  | --- | --- |
39
- | [gates.md](gates.md) | **I changed some files — which gate do I run?** |
81
+ | [gates.md](gates.md) | **I changed some files. Which gate do I run?** |
40
82
  | [gate-plan.md](gate-plan.md) | why the gates are shaped this way, and what is still unbuilt |
41
83
  | [system-map.md](system-map.md) | how a tool call reaches the app, and which failures are silent |
42
84
  | [telemetry-contract.md](telemetry-contract.md) | required reading before touching anything that emits |
43
- | [debugging.md](debugging.md) | debugging Reticle itself — the four signals and what each answers |
85
+ | [telemetry-events.mdx](telemetry-events.mdx) | every event kind and its payload, and the question each one exists to answer |
86
+ | [debugging.md](debugging.md) | debugging Reticle itself: the four signals and what each answers |
44
87
  | [fixtures.md](fixtures.md) | the sibling `reticle-fixtures` repo: install complexity, not regressions |
45
- | [matrix/README.md](matrix/README.md) | submitting an MCP-client compatibility record — the best first contribution |
88
+ | [matrix/README.md](matrix/README.md) | submitting an MCP-client compatibility record, the best first contribution |
46
89
  | [matrix/MATRIX.md](matrix/MATRIX.md) | which clients are known to work (generated; do not hand-edit) |
47
90
  | [`../apps/README.md`](../apps/README.md) | what belongs in `apps/`, and what does not |
48
91
  | [`../apps/e2e/harness-rules.md`](../apps/e2e/harness-rules.md) | the four rules every gate obeys, and the incident behind each |
49
92
  | [`../bench/README.md`](../bench/README.md) | what in `bench/` is live and what is a kept one-off study |
50
93
 
51
- `fixtures-dispatch-receiver.yml` is not a doc — it is the workflow template `reticle-fixtures` needs, kept in this repo so the two ends of the contract cannot drift apart.
94
+ `fixtures-dispatch-receiver.yml` is not a doc; it is the workflow template `reticle-fixtures` needs, kept in this repo so the two ends of the contract cannot drift apart.
52
95
 
53
96
  ---
54
97
 
55
- **Adding a page?** Put it in the right table above. If it is user-facing, add it to `mint.json` too — a page that is published but unlisted is a page nobody finds, and a page listed here but absent from `mint.json` is one users cannot reach at all.
98
+ **Adding a page?** Three things, all required. Give it `title` + `description` frontmatter and no `# H1`. Mintlify renders the frontmatter title as the heading, so an H1 shows up twice. Add its slug to the right tab in `docs.json`. Add a row to the right table above. A page missing from `docs.json` is one nobody can reach; a page missing from this index is one no contributor knows exists.
99
+
100
+ **Adding a second product?** Do not nest these files under a product folder pre-emptively; that renames every live URL to buy nothing. Mintlify supports `navigation.products`, where each product declares its own `directory`. When the second product exists, put its pages in `docs/<product>/`, switch `docs.json` from `tabs` to `products`, and give this product `"directory": ""` so today's URLs keep working.
@@ -0,0 +1,87 @@
1
+ ---
2
+ title: Action reference: lights, camera, click
3
+ sidebarTitle: Actions
4
+ description: Every action reticle_act and reticle_act_and_wait can perform, with its arguments and the one that has a history.
5
+ icon: hand-pointer
6
+ ---
7
+
8
+ ```json
9
+ { "ref": "e42", "action": "fill", "args": { "value": "hello" } }
10
+ ```
11
+
12
+ | Action | Arguments | Notes |
13
+ | --- | --- | --- |
14
+ | `click` / `dblclick` | none | Dispatches a real click |
15
+ | `hover` | none | `mouseover` + `mouseenter`, so JS hover state triggers |
16
+ | `focus` / `blur` | none | |
17
+ | `fill` | `{ value }` | Sets value via a React-safe native setter, then `input` and `change` |
18
+ | `type` | `{ text }` | **Appends** to the current value |
19
+ | `clear` | none | Empties an input |
20
+ | `select` | `{ value }` | For `<select>` |
21
+ | `check` / `uncheck` | none | Checkbox and radio |
22
+ | `submit` | none | Submits the element's `<form>` |
23
+ | `press` | `{ text }` | The key **name**: `Escape`, `Tab`, `Enter` |
24
+ | `scrollIntoView` | none | |
25
+ | `upload` | `{ name, content?, type? }` | Sets a file on `<input type="file">` |
26
+ | `drag` | `{ toRef }` | Pointer-based drag (dnd-kit, react-beautiful-dnd) plus HTML5 DnD |
27
+ | `webmcp` | `{ tool, params }` | Calls a `navigator.modelContext` tool, if the site exposes one |
28
+
29
+ ## `fill` versus `type`
30
+
31
+ `fill` replaces. `type` appends. Reaching for `type` when you meant `fill` is how a field ends up containing `hellohello`, and neither call will complain.
32
+
33
+ ## `press` takes `text`, not `key`
34
+
35
+ Both are accepted today, and `text` wins when both are present. Use `text`. It is what the tool description documents.
36
+
37
+ <Warning>
38
+ This one has history worth knowing. The implementation used to read `args.key` and **default to
39
+ `Enter`**, so the documented call, `{ action: "press", args: { text: "Escape" } }`, silently sent
40
+ `Enter` and reported success.
41
+
42
+ Three consequences, worst last: the requested key never arrived, so Escape-to-close and Tab-traversal went unverified while looking verified; nothing in the result said the argument had been ignored; and `Enter` is not a neutral substitute. On a focused field inside a form it **submits it**. A request to close a dialog could file the form behind it.
43
+
44
+ Two field reports were exactly this, both diagnosed as "synthetic events don't reach the app". Wrong root cause: the event reached the app perfectly well and simply said `Enter`.
45
+
46
+ </Warning>
47
+
48
+ That bug is fixed, and the reason it is written down here is that it is the exact shape of failure Reticle exists to catch. An action that reports success while doing something else.
49
+
50
+ ## Batching
51
+
52
+ Several actions in one round trip:
53
+
54
+ ```json
55
+ {
56
+ "steps": [
57
+ { "ref": "e101", "action": "fill", "args": { "value": "admin@reticle.dev" } },
58
+ { "ref": "e102", "action": "fill", "args": { "value": "hunter2" } },
59
+ { "ref": "e103", "action": "click" }
60
+ ]
61
+ }
62
+ ```
63
+
64
+ <Warning>
65
+ [`reticle_act_sequence`](/tools-act-sequence) returns `ok: true` when the steps were
66
+ **dispatched**, not when they worked. Batch the setup, then prove the outcome with
67
+ [`reticle_act_and_wait`](/tools-act-and-wait) on the final step.
68
+ </Warning>
69
+
70
+ ## Real versus synthetic input
71
+
72
+ `inputMode` in the response says which you got. `synthetic` means events dispatched programmatically. Correct for the overwhelming majority of apps. `real` means native CDP input, available under `reticle drive`, and needed by a few drag-and-drop and pointer-gesture libraries.
73
+
74
+ <Warning>
75
+ On a throttled tab, synthetic timers and pointer gestures can silently no-op. Pass
76
+ `refuseWhenThrottled: true` to fail loudly instead of acting into a tab the browser has paused.
77
+ </Warning>
78
+
79
+ ## What every action reports
80
+
81
+ `valueChanged`, `focusMoved`, `domMutatedWithin`, `defaultPrevented`, `occluded` and `occludedBy`. Plus the element's `testid`, `component` and `source` file and line.
82
+
83
+ `occluded` is the answer to a large share of "the button doesn't work" reports: the click landed on the modal overlay sitting on top of it.
84
+
85
+ <Card title="Full field-by-field walkthrough" icon="hand-pointer" href="/tools-act">
86
+ What each effect field tells you, with a real response.
87
+ </Card>