@reticlehq/server 3.3.0 → 3.4.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 (299) hide show
  1. package/LICENSE-ENTERPRISE +1 -1
  2. package/README.md +3 -3
  3. package/SKILL.md +12 -14
  4. package/dist/chromium-launch-options.d.ts +4 -3
  5. package/dist/chromium-launch-options.js +6 -2
  6. package/dist/command/cli/adhoc-suite.d.ts +22 -0
  7. package/dist/command/cli/adhoc-suite.js +90 -19
  8. package/dist/command/cli/adhoc-verdict.d.ts +35 -0
  9. package/dist/command/cli/adhoc-verdict.js +90 -3
  10. package/dist/command/cli/answers/gate-hook-message.d.ts +6 -0
  11. package/dist/command/cli/answers/gate-hook-message.js +6 -0
  12. package/dist/command/cli/auth/cloud-keystore.d.ts +0 -2
  13. package/dist/command/cli/auth/cloud-keystore.js +0 -3
  14. package/dist/command/cli/auth/cloud-session.d.ts +4 -0
  15. package/dist/command/cli/auth/cloud-session.js +2 -0
  16. package/dist/command/cli/cli-doctor.js +7 -5
  17. package/dist/command/cli/cli-flow-commands.d.ts +3 -6
  18. package/dist/command/cli/cli-flow-commands.js +67 -4
  19. package/dist/command/cli/cli-parse.d.ts +8 -1
  20. package/dist/command/cli/cli-parse.js +21 -3
  21. package/dist/command/cli/cli-update-commands.js +8 -1
  22. package/dist/command/cli/cli-verify.d.ts +24 -3
  23. package/dist/command/cli/cli-verify.js +54 -11
  24. package/dist/command/cli/cloud-cli.js +5 -2
  25. package/dist/command/cli/cloud-kit.js +3 -5
  26. package/dist/command/cli/cloud-login.js +21 -3
  27. package/dist/command/cli/demo/demo-page.js +6 -3
  28. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +5 -0
  29. package/dist/command/cli/doctor/browser/chromium-hint.js +5 -0
  30. package/dist/command/cli/launch/cli-launch.d.ts +8 -0
  31. package/dist/command/cli/launch/cli-launch.js +21 -9
  32. package/dist/command/cli/launch/sdk-version-match.d.ts +84 -0
  33. package/dist/command/cli/launch/sdk-version-match.js +206 -0
  34. package/dist/command/cli/lifecycle/daemon-lifecycle.js +2 -16
  35. package/dist/command/cli/ports/resolve/cli-port.d.ts +25 -0
  36. package/dist/command/cli/ports/resolve/cli-port.js +125 -15
  37. package/dist/command/cli/setup-mcp-cli.js +13 -13
  38. package/dist/command/cli/tutorial.d.ts +2 -2
  39. package/dist/command/cli/tutorial.js +21 -7
  40. package/dist/command/cli.js +62 -10
  41. package/dist/command/daemon/binding/daemon-status-probe.d.ts +14 -2
  42. package/dist/command/daemon/binding/daemon-status-probe.js +34 -6
  43. package/dist/command/daemon/daemon-resolve.d.ts +9 -0
  44. package/dist/command/daemon/daemon-resolve.js +1 -1
  45. package/dist/command/daemon/lifetime/daemon-usefulness.d.ts +7 -1
  46. package/dist/command/daemon/lifetime/daemon-usefulness.js +30 -7
  47. package/dist/command/dev/stale-issue-guard.d.ts +3 -4
  48. package/dist/command/dev/stale-issue-guard.js +8 -8
  49. package/dist/command/setup/bringup/connect-lease.d.ts +17 -0
  50. package/dist/command/setup/bringup/connect-lease.js +38 -0
  51. package/dist/command/setup/bringup/dev-server-files.d.ts +68 -0
  52. package/dist/command/setup/bringup/dev-server-files.js +131 -0
  53. package/dist/command/setup/bringup/dev-server-log-cap.d.ts +35 -0
  54. package/dist/command/setup/bringup/dev-server-log-cap.js +121 -0
  55. package/dist/command/setup/bringup/ensure-daemon.d.ts +65 -4
  56. package/dist/command/setup/bringup/ensure-daemon.js +135 -16
  57. package/dist/command/setup/bringup/static-page-server.d.ts +35 -0
  58. package/dist/command/setup/bringup/static-page-server.js +151 -0
  59. package/dist/command/setup/desktop-shape.d.ts +4 -0
  60. package/dist/command/setup/desktop-shape.js +18 -2
  61. package/dist/command/setup/init/init-port.d.ts +15 -0
  62. package/dist/command/setup/init/init-port.js +44 -0
  63. package/dist/command/setup/init/init-runtime.d.ts +1 -1
  64. package/dist/command/setup/init/init-runtime.js +39 -2
  65. package/dist/command/setup/node-effects.d.ts +27 -14
  66. package/dist/command/setup/node-effects.js +80 -44
  67. package/dist/command/setup/probe/loopback-probe-urls.d.ts +7 -0
  68. package/dist/command/setup/probe/loopback-probe-urls.js +40 -0
  69. package/dist/command/setup/probe/page-probe.d.ts +13 -2
  70. package/dist/command/setup/probe/page-probe.js +18 -3
  71. package/dist/command/setup/run-setup.d.ts +26 -1
  72. package/dist/command/setup/run-setup.js +96 -13
  73. package/dist/command/setup/session-pick.d.ts +13 -0
  74. package/dist/command/setup/session-pick.js +20 -0
  75. package/dist/command/setup/setup-command.d.ts +2 -14
  76. package/dist/command/setup/setup-command.js +105 -31
  77. package/dist/command/setup/setup-mcp.js +3 -3
  78. package/dist/command/setup/terminal/wait-progress.d.ts +8 -0
  79. package/dist/command/setup/terminal/wait-progress.js +17 -0
  80. package/dist/features/crawl/crawl-empty.js +1 -1
  81. package/dist/features/crawl/crawl-tools.d.ts +6 -0
  82. package/dist/features/crawl/crawl-tools.js +57 -0
  83. package/dist/features/crawl/crawl.js +171 -157
  84. package/dist/features/exhaust/app-coverage.d.ts +33 -0
  85. package/dist/features/exhaust/app-coverage.js +31 -0
  86. package/dist/features/exhaust/code-coverage.d.ts +47 -0
  87. package/dist/features/exhaust/code-coverage.js +77 -0
  88. package/dist/features/exhaust/exhaust-run.d.ts +33 -0
  89. package/dist/features/exhaust/exhaust-run.js +82 -0
  90. package/dist/features/exhaust/explorer.d.ts +103 -0
  91. package/dist/features/exhaust/explorer.js +195 -0
  92. package/dist/features/exhaust/failure-branches.d.ts +33 -0
  93. package/dist/features/exhaust/failure-branches.js +92 -0
  94. package/dist/features/exhaust/ledger.d.ts +202 -0
  95. package/dist/features/exhaust/ledger.js +252 -0
  96. package/dist/features/exhaust/session-fold.d.ts +31 -0
  97. package/dist/features/exhaust/session-fold.js +79 -0
  98. package/dist/features/exhaust/session-port.d.ts +36 -0
  99. package/dist/features/exhaust/session-port.js +102 -0
  100. package/dist/features/harness/drive-report.d.ts +0 -9
  101. package/dist/features/harness/drive-report.js +18 -0
  102. package/dist/features/harness/driver.js +4 -7
  103. package/dist/features/harness/fill-values.d.ts +6 -0
  104. package/dist/features/harness/fill-values.js +3 -0
  105. package/dist/features/harness/jev-driver.js +30 -13
  106. package/dist/features/harness/openai-driver.js +4 -7
  107. package/dist/features/harness/platform-config.js +5 -6
  108. package/dist/index.d.ts +9 -0
  109. package/dist/index.js +40 -11
  110. package/dist/judgement/domain/domain-model.js +3 -1
  111. package/dist/judgement/oracles/self-instrument.js +4 -2
  112. package/dist/judgement/runs/artifact/replay-mapping.d.ts +2 -2
  113. package/dist/judgement/runs/artifact/replay-mapping.js +44 -9
  114. package/dist/judgement/runs/verification-sync.d.ts +3 -1
  115. package/dist/judgement/runs/verification-sync.js +1 -1
  116. package/dist/language/flows/change/affected.d.ts +9 -0
  117. package/dist/language/flows/change/affected.js +35 -0
  118. package/dist/language/flows/change/gate.d.ts +20 -0
  119. package/dist/language/flows/change/gate.js +12 -1
  120. package/dist/language/flows/flow-author.d.ts +7 -0
  121. package/dist/language/flows/flow-author.js +19 -0
  122. package/dist/language/flows/flow-expect-grammar.js +1 -1
  123. package/dist/language/flows/flow-journey.d.ts +54 -0
  124. package/dist/language/flows/flow-journey.js +119 -0
  125. package/dist/language/flows/flow-replay-run.d.ts +2 -3
  126. package/dist/language/flows/flow-replay-run.js +13 -47
  127. package/dist/language/flows/flow-replay.d.ts +22 -8
  128. package/dist/language/flows/flow-replay.js +55 -9
  129. package/dist/language/flows/flow-result.d.ts +2 -0
  130. package/dist/language/flows/flow-result.js +25 -0
  131. package/dist/language/flows/flow-tools.js +17 -5
  132. package/dist/language/flows/flows.d.ts +10 -1
  133. package/dist/language/flows/flows.js +90 -14
  134. package/dist/language/flows/recording/replay-halt.js +3 -1
  135. package/dist/language/flows/recording/tape/recordings.d.ts +30 -1
  136. package/dist/language/flows/recording/tape/recordings.js +55 -1
  137. package/dist/language/flows/replay.d.ts +19 -0
  138. package/dist/language/flows/replay.js +42 -6
  139. package/dist/language/flows/verify-change-tools.js +5 -2
  140. package/dist/launch-chromium.d.ts +80 -0
  141. package/dist/launch-chromium.js +218 -0
  142. package/dist/memory/cloud/cloud-config.d.ts +11 -2
  143. package/dist/memory/cloud/cloud-config.js +47 -7
  144. package/dist/memory/cloud/cloud-sync.d.ts +4 -1
  145. package/dist/memory/cloud/cloud-sync.js +6 -9
  146. package/dist/memory/cloud/harness-offer.js +5 -6
  147. package/dist/memory/cloud/harness-switch.js +5 -6
  148. package/dist/memory/cloud/sync-cycle.d.ts +68 -3
  149. package/dist/memory/cloud/sync-cycle.js +298 -55
  150. package/dist/memory/cloud/sync-daemon.js +24 -20
  151. package/dist/memory/cloud/sync-disk.js +16 -2
  152. package/dist/memory/intent/intent-shard.d.ts +10 -32
  153. package/dist/memory/intent/intent-shard.js +0 -10
  154. package/dist/memory/journal/drive-flow.d.ts +18 -13
  155. package/dist/memory/journal/drive-flow.js +45 -37
  156. package/dist/memory/journal/on-disk/workspace-tiers.js +2 -0
  157. package/dist/memory/journal/session-end.d.ts +9 -1
  158. package/dist/memory/journal/session-end.js +24 -12
  159. package/dist/memory/project/dir/reticle-dir.d.ts +2 -0
  160. package/dist/memory/project/dir/reticle-dir.js +1 -0
  161. package/dist/memory/project/project-tools.js +3 -1
  162. package/dist/portal/bridge/auth-failure-reason.d.ts +1 -1
  163. package/dist/portal/bridge/auth-failure-reason.js +4 -2
  164. package/dist/portal/bridge/bridge.d.ts +8 -0
  165. package/dist/portal/bridge/bridge.js +43 -2
  166. package/dist/portal/input/js-coverage.d.ts +25 -0
  167. package/dist/portal/input/js-coverage.js +30 -0
  168. package/dist/portal/input/real-input.d.ts +11 -0
  169. package/dist/portal/input/real-input.js +29 -4
  170. package/dist/portal/pool/playwright-launcher.d.ts +6 -0
  171. package/dist/portal/pool/playwright-launcher.js +20 -13
  172. package/dist/portal/session/dev-server/dev-command.d.ts +2 -2
  173. package/dist/portal/session/dev-server/dev-command.js +30 -12
  174. package/dist/portal/session/dev-server/dev-server-probe.js +17 -1
  175. package/dist/portal/session/dev-server/route-status-probe.d.ts +24 -0
  176. package/dist/portal/session/dev-server/route-status-probe.js +57 -0
  177. package/dist/portal/session/facts/observed-state.d.ts +11 -0
  178. package/dist/portal/session/facts/observed-state.js +34 -0
  179. package/dist/portal/session/no-session-diagnosis.d.ts +29 -0
  180. package/dist/portal/session/no-session-diagnosis.js +62 -5
  181. package/dist/portal/session/no-session-next-action.d.ts +13 -0
  182. package/dist/portal/session/no-session-next-action.js +48 -9
  183. package/dist/portal/session/no-session-watch.d.ts +18 -0
  184. package/dist/portal/session/no-session-watch.js +88 -2
  185. package/dist/portal/session/session-health.js +3 -2
  186. package/dist/portal/session/session-manager.d.ts +20 -6
  187. package/dist/portal/session/session-manager.js +19 -3
  188. package/dist/portal/session/session-reaper.d.ts +28 -0
  189. package/dist/portal/session/session-reaper.js +49 -1
  190. package/dist/portal/session/session.d.ts +5 -0
  191. package/dist/portal/session/session.js +15 -19
  192. package/dist/surface/http-server.d.ts +2 -1
  193. package/dist/surface/http-server.js +4 -2
  194. package/dist/surface/mcp/mcp-proxy.d.ts +2 -2
  195. package/dist/surface/mcp/mcp-proxy.js +2 -2
  196. package/dist/surface/mcp/mcp.js +4 -2
  197. package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +13 -1
  198. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +27 -2
  199. package/dist/surface/mcp/server-instructions.js +9 -7
  200. package/dist/surface/tools/act/after-match.d.ts +17 -0
  201. package/dist/surface/tools/act/after-match.js +30 -0
  202. package/dist/surface/tools/act/durable.d.ts +69 -0
  203. package/dist/surface/tools/act/durable.js +139 -0
  204. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +13 -6
  205. package/dist/surface/tools/act/navigation/navigate-arrival.js +47 -10
  206. package/dist/surface/tools/act/navigation/navigate-result.d.ts +1 -12
  207. package/dist/surface/tools/act/navigation/navigate-result.js +21 -3
  208. package/dist/surface/tools/act/proved-controls.d.ts +9 -0
  209. package/dist/surface/tools/act/proved-controls.js +11 -0
  210. package/dist/surface/tools/act/sequence-grade.d.ts +5 -0
  211. package/dist/surface/tools/act/sequence-grade.js +32 -1
  212. package/dist/surface/tools/act-capsule.d.ts +7 -0
  213. package/dist/surface/tools/act-capsule.js +46 -1
  214. package/dist/surface/tools/act-sequence-tool.js +25 -8
  215. package/dist/surface/tools/act-tools.js +26 -50
  216. package/dist/surface/tools/assert/assert-grade.js +12 -1
  217. package/dist/surface/tools/assert/assert-verdict.js +2 -2
  218. package/dist/surface/tools/browser-tools.js +14 -2
  219. package/dist/surface/tools/coverage-tools.js +27 -1
  220. package/dist/surface/tools/error-recovery.js +4 -4
  221. package/dist/surface/tools/feedback-invite.js +6 -4
  222. package/dist/surface/tools/harness-explore.d.ts +3 -2
  223. package/dist/surface/tools/harness-explore.js +7 -6
  224. package/dist/surface/tools/lease-tools.js +15 -6
  225. package/dist/surface/tools/live-call-text.js +4 -1
  226. package/dist/surface/tools/observe-tools.js +9 -1
  227. package/dist/surface/tools/tools.js +4 -0
  228. package/dist/telemetry/argument-shape.d.ts +0 -2
  229. package/dist/telemetry/argument-shape.js +0 -4
  230. package/dist/telemetry/error-class.d.ts +2 -3
  231. package/dist/telemetry/error-class.js +2 -3
  232. package/dist/telemetry/harness-drive.d.ts +0 -2
  233. package/dist/telemetry/harness-drive.js +0 -4
  234. package/dist/wire-journal.d.ts +3 -0
  235. package/dist/wire-journal.js +4 -0
  236. package/dist/workspace-packages.d.ts +0 -2
  237. package/dist/workspace-packages.js +1 -9
  238. package/docs/README.md +4 -2
  239. package/docs/actions.mdx +3 -3
  240. package/docs/agent-cheatsheet.md +4 -4
  241. package/docs/architecture.md +6 -6
  242. package/docs/best-practices.mdx +1 -1
  243. package/docs/capabilities.mdx +32 -1
  244. package/docs/cli/cloud.mdx +14 -4
  245. package/docs/cli/doctor.mdx +4 -0
  246. package/docs/cli/feedback.mdx +2 -2
  247. package/docs/cli/gate.mdx +9 -4
  248. package/docs/cli/update.mdx +10 -0
  249. package/docs/cli.mdx +9 -0
  250. package/docs/desktop.mdx +14 -10
  251. package/docs/environment.mdx +87 -0
  252. package/docs/faq.mdx +18 -16
  253. package/docs/first-drive.md +5 -13
  254. package/docs/flows.md +21 -2
  255. package/docs/frameworks.mdx +29 -10
  256. package/docs/getting-started.md +3 -3
  257. package/docs/http-transport.md +6 -3
  258. package/docs/human-control.md +2 -2
  259. package/docs/index.mdx +16 -6
  260. package/docs/install-agentic.mdx +1 -1
  261. package/docs/install-manual.mdx +1 -1
  262. package/docs/integration-patterns.md +3 -3
  263. package/docs/multi-agent-testing.md +1 -1
  264. package/docs/packages/babel-plugin.mdx +1 -1
  265. package/docs/packages/browser.mdx +2 -2
  266. package/docs/packages/react.mdx +2 -2
  267. package/docs/packages/tauri.mdx +1 -1
  268. package/docs/packages/vite-plugin.mdx +2 -2
  269. package/docs/packages.mdx +9 -9
  270. package/docs/predicates.mdx +50 -5
  271. package/docs/quickstart.mdx +2 -2
  272. package/docs/recipes.mdx +2 -2
  273. package/docs/state-management.mdx +2 -2
  274. package/docs/system-map.md +1 -1
  275. package/docs/telemetry.md +1 -1
  276. package/docs/token-efficiency.md +3 -3
  277. package/docs/tools/act-and-wait.mdx +1 -0
  278. package/docs/tools/act-sequence.mdx +1 -1
  279. package/docs/tools/console.mdx +1 -1
  280. package/docs/tools/inspect.mdx +1 -1
  281. package/docs/tools/network.mdx +1 -1
  282. package/docs/tools/query.mdx +1 -1
  283. package/docs/tools/sessions.mdx +1 -1
  284. package/docs/tools/snapshot.mdx +1 -1
  285. package/docs/tools/state.mdx +1 -1
  286. package/docs/tools/wait-for.mdx +1 -1
  287. package/docs/usage.md +7 -6
  288. package/docs/use-cases.mdx +157 -0
  289. package/docs/vs-screenshots.mdx +3 -3
  290. package/docs/what-is-recorded.md +2 -2
  291. package/docs/what-is-reticle.mdx +17 -20
  292. package/docs/why-reticle.mdx +14 -21
  293. package/package.json +10 -9
  294. package/dist/judgement/capsule/minimize.d.ts +0 -12
  295. package/dist/judgement/capsule/minimize.js +0 -22
  296. package/dist/judgement/phenomena/phenomena.d.ts +0 -27
  297. package/dist/judgement/phenomena/phenomena.js +0 -101
  298. package/dist/language/flows/flow-report.d.ts +0 -22
  299. package/dist/language/flows/flow-report.js +0 -106
@@ -13,7 +13,7 @@ export const BROWSER_TOOLS = [
13
13
  {
14
14
  name: ReticleTool.NAVIGATE,
15
15
  example: { url: '/settings' },
16
- description: 'Navigate the connected browser tab to a URL, or reload it in place with { reload: true } (add { hard: true } to bypass the cache). `ok` means the navigation was DISPATCHED — the SDK is torn down by the navigation, so the page itself cannot report on it. The daemon then waits up to `timeout_ms` (default 5000) for the SDK to reconnect: `confirmed:true` with a new `sessionId` means the page arrived and you can act immediately. `confirmed:false` means it did not arrive within that window (reported as `waitedMs`) — the page may be slow, uninstrumented, or not there. A slow SPA can take 30-60s to reattach: raise `timeout_ms` rather than polling reticle_sessions.',
16
+ description: 'Navigate the connected browser tab to a URL, or reload it in place with { reload: true } (add { hard: true } to bypass the cache). `ok` means the navigation was DISPATCHED — the SDK is torn down by the navigation, so the old page cannot report on it. The daemon then waits up to `timeout_ms` (default 5000) for the SDK to reconnect. `confirmed:true` with a `sessionId` means the requested page arrived and you can act immediately. `confirmed:false` with `landedOn` means the navigation arrived but the app landed at another URL, such as an authentication or route-guard redirect; use `landedOn` to understand where the browser actually ended up. `confirmed:false` without `landedOn` means the target was not observed within the wait window; a slow app may still be on its way, so increase `timeout_ms` when appropriate.',
17
17
  inputSchema: {
18
18
  url: z.string().optional().describe('The URL to navigate to. Omit when using reload.'),
19
19
  reload: z
@@ -38,6 +38,10 @@ export const BROWSER_TOOLS = [
38
38
  confirmed: z.boolean().optional(),
39
39
  /** The session the SDK reconnected as, when arrival was confirmed — it is a NEW id. */
40
40
  sessionId: z.string().optional(),
41
+ /**
42
+ * The URL where the navigated document landed when it differs from the requested target.
43
+ */
44
+ landedOn: z.string().optional(),
41
45
  /**
42
46
  * On confirmed:false — the budget (ms) that expired without the page coming back. Says which
43
47
  * of "Reticle stopped waiting" and "the page never came back" you are looking at.
@@ -58,6 +62,7 @@ export const BROWSER_TOOLS = [
58
62
  // back past this point. Reported from the field as an assertion whose clauses all passed
59
63
  // coming back `contradicted` by hundreds of 500s against resources that were already gone.
60
64
  before.lastAct.markNavigated(before.elapsed());
65
+ deps.recordings.markNavigated();
61
66
  await commandOrThrow(deps, asString(args['sessionId']), ReticleCommand.REFRESH, {
62
67
  hard: true === args['hard'],
63
68
  });
@@ -96,6 +101,7 @@ export const BROWSER_TOOLS = [
96
101
  // document just as thoroughly. `beginAction` attributes events to this action; it does not move
97
102
  // the window a later assert judges over, and those are two different jobs.
98
103
  session.lastAct.markNavigated(session.elapsed());
104
+ deps.recordings.markNavigated();
99
105
  try {
100
106
  const result = (await commandOrThrow(deps, asString(args['sessionId']), ReticleCommand.NAVIGATE, { url }));
101
107
  // `ok` is the browser accepting the instruction, not the page arriving — see navigate-result.
@@ -104,7 +110,13 @@ export const BROWSER_TOOLS = [
104
110
  // reticle_sessions itself. A refusal skips the wait: there is nothing to wait FOR.
105
111
  const timeoutMs = asNumber(args['timeout_ms']) ?? ARRIVAL_TIMEOUT_MS;
106
112
  const arrival = true === result.ok
107
- ? await awaitArrival(deps.sessions, target, { navigatedId: session.id, priorIds }, timeoutMs)
113
+ ? await awaitArrival(deps.sessions, target, {
114
+ // Keep the original Session object and URL so arrival detection can distinguish
115
+ // an in-progress navigation from a document that arrived somewhere else.
116
+ navigatedSession: session,
117
+ navigatedFrom: session.url,
118
+ priorIds,
119
+ }, timeoutMs)
108
120
  : null;
109
121
  return navigateResult(result, arrival, timeoutMs);
110
122
  }
@@ -10,6 +10,7 @@ import { ReticleTool } from '@reticlehq/core';
10
10
  import { asString } from '@reticlehq/core';
11
11
  import { exercisedCount } from './gaps/coverage-identity.js';
12
12
  import { commandOrThrow, sessionIdShape } from './tool-kit.js';
13
+ import { foldAppCoverage } from '../../features/exhaust/app-coverage.js';
13
14
  /**
14
15
  * `reticle_coverage` — which interactive controls this session has driven, and which it has not.
15
16
  *
@@ -69,8 +70,17 @@ export function buildCoverageTools(toolNames) {
69
70
  name: ReticleTool.COVERAGE,
70
71
  description: 'Which interactive controls you have driven this session, and which you have NOT. Returns { total, exercised, untouched:[{ref,label}], alsoDroveGone? } over the controls currently on the page. Use it to decide whether verification is finished: an untouched list that still holds the controls your change affects means you are not done. This is about what you did not TOUCH — distinct from the `coverage` field on an action result, which reports what the layer could not SEE.',
71
72
  example: {},
72
- inputSchema: { ...sessionIdShape },
73
+ inputSchema: {
74
+ app: z
75
+ .boolean()
76
+ .optional()
77
+ .describe('Also fold into the app-wide ledger; returns `app.levels`.'),
78
+ ...sessionIdShape,
79
+ },
73
80
  outputSchema: {
81
+ // The app-wide ledger — see features/exhaust/ledger.ts. Loose: run-only, and the levels
82
+ // carry their own shape.
83
+ app: z.record(z.unknown()).optional(),
74
84
  total: z.number(),
75
85
  exercised: z.number(),
76
86
  untouched: z.array(z.object({ ref: z.string(), label: z.string() })),
@@ -171,6 +181,22 @@ export function buildCoverageTools(toolNames) {
171
181
  ...(regressed === undefined ? {} : { observabilityRegressed: regressed }),
172
182
  featureUse,
173
183
  toolHitRate,
184
+ ...(true === args['app']
185
+ ? {
186
+ app: await foldAppCoverage({
187
+ fs: deps.fs,
188
+ reticleRoot: sessionRoot(deps, sessionId),
189
+ seen: parseControls(tree).map((c) => c.label),
190
+ session,
191
+ now: deps.now,
192
+ ...(deps.realInput?.takeCodeCoverage === undefined
193
+ ? {}
194
+ : {
195
+ takeCode: (url) => deps.realInput?.takeCodeCoverage?.(url) ?? Promise.resolve(undefined),
196
+ }),
197
+ }),
198
+ }
199
+ : {}),
174
200
  };
175
201
  },
176
202
  },
@@ -67,7 +67,7 @@ export const RECOVERY = {
67
67
  UNKNOWN_SESSION: 'That sessionId is not connected. Call reticle_sessions for the current ids and retry with a valid one.',
68
68
  THROTTLED: 'The target tab is backgrounded/throttled, so actions may silently no-op. Ask the human to bring ' +
69
69
  'the tab to the front, or acquire a guaranteed scriptable context yourself with ' +
70
- '`reticle_run { tool: "reticle_lease", action: "acquire", url }` (a human can equivalently run ' +
70
+ '`reticle_run { tool: "reticle_lease", args: { action: "acquire", url } }` (a human can equivalently run ' +
71
71
  '`reticle drive <url>`).',
72
72
  MISSING_BASELINE: 'That baseline does not exist yet. Call reticle_baseline { action: "list" } to see saved names, or ' +
73
73
  'reticle_baseline { action: "save", name } to capture one before diffing against it.',
@@ -159,7 +159,7 @@ export const RECOVERY = {
159
159
  'assert on the result instead of typing it. Known gap, already reported: no need to file it.',
160
160
  HOVER_NEEDS_POINTER: 'Hover needs a real pointer: a synthetic mouseover does not apply CSS :hover, and Reticle ' +
161
161
  'refuses rather than reporting the styles as applied. Acquire a tab with ' +
162
- 'reticle_run { tool: "reticle_lease", action: "acquire", url } — reticle_lease is not ' +
162
+ 'reticle_run { tool: "reticle_lease", args: { action: "acquire", url } } — reticle_lease is not ' +
163
163
  'advertised under the default profile, so it is reached through reticle_run, not called ' +
164
164
  'directly — or ask the human to drive with `reticle drive` / RETICLE_CDP_URL. This is a ' +
165
165
  'deliberate refusal, not a defect: there is nothing to report.',
@@ -182,7 +182,7 @@ export const RECOVERY = {
182
182
  COMMAND_TIMEOUT: 'The page did not answer within the command window. That is a fact about the page, not a Reticle ' +
183
183
  'failure: check reticle_sessions for `throttled`/`stale` on this session — a backgrounded tab is ' +
184
184
  'throttled and may never answer, so ask the human to bring it to the front or drive your own ' +
185
- 'browser with reticle_run { tool: "reticle_lease", action: "acquire", url } — reticle_lease is ' +
185
+ 'browser with reticle_run { tool: "reticle_lease", args: { action: "acquire", url } } — reticle_lease is ' +
186
186
  'not advertised under the default profile, so it is reached through reticle_run, not called ' +
187
187
  'directly. If the tab is in front, the page is busy or blocked (a long ' +
188
188
  'synchronous task, an alert/confirm dialog); reticle_console usually shows what it hit. Retry ' +
@@ -495,7 +495,7 @@ const INVALID_NAME_REJECTION = /^invalid (?:[a-z]+ )*name: /i;
495
495
  * us nothing new.
496
496
  */
497
497
  export const FEEDBACK_ASK = 'This error is not one Reticle recognizes, which means it may be a defect in Reticle rather than in ' +
498
- 'the app. If you believe Reticle misbehaved, call reticle_feedback with a root-cause analysis and ' +
498
+ 'the app. If you believe Reticle misbehaved, call reticle_session { action: "feedback" } with a root-cause analysis and ' +
499
499
  'the call trace before moving on — that report is the only way this gets fixed.';
500
500
  /**
501
501
  * Build the tool-error payload spliced at the MCP boundary. `recovery` is added when the error is
@@ -36,12 +36,14 @@ export const FrictionKind = {
36
36
  * a run of three is where a deliberate retry becomes a loop.
37
37
  */
38
38
  const REPEAT_INVITE_AT = 3;
39
+ /** The call that files feedback. The old `reticle_feedback` name only answers with a redirect. */
40
+ const FEEDBACK_CALL = 'reticle_session { action: "feedback" }';
39
41
  const LINES = {
40
- [FrictionKind.UNKNOWN_TOOL]: 'no such tool — reticle_feedback tells us what you expected it to do, which is how it gets built',
41
- [FrictionKind.UNKNOWN_VERDICT]: 'Reticle could not tell what happened here. If you expected otherwise, reticle_feedback. An ' +
42
+ [FrictionKind.UNKNOWN_TOOL]: `no such tool — ${FEEDBACK_CALL} tells us what you expected it to do, which is how it gets built`,
43
+ [FrictionKind.UNKNOWN_VERDICT]: `Reticle could not tell what happened here. If you expected otherwise, ${FEEDBACK_CALL}. An ` +
42
44
  'unknown verdict is our defect, not yours',
43
- [FrictionKind.REPEATING]: 'stuck on the same call? reticle_feedback — say what you were trying to do and it gets fixed',
44
- [FrictionKind.REFUSED]: 'if that diagnosis was wrong or unhelpful, reticle_feedback — error messages are a product ' +
45
+ [FrictionKind.REPEATING]: `stuck on the same call? ${FEEDBACK_CALL} — say what you were trying to do and it gets fixed`,
46
+ [FrictionKind.REFUSED]: `if that diagnosis was wrong or unhelpful, ${FEEDBACK_CALL} — error messages are a product ` +
45
47
  'surface and we fix the ones that fail',
46
48
  };
47
49
  /**
@@ -98,8 +98,9 @@ export declare const MSG_NO_OPENAI_KEY: string;
98
98
  *
99
99
  * Resolved into the env rather than threaded through four call sites: the drivers and the
100
100
  * preference lookup all read an env record, and giving them a completed one leaves each of them
101
- * exactly as simple as it was. An EXPLICIT variable still wins — someone who exported a key meant
102
- * that key, and CI has no linked project to read.
101
+ * exactly as simple as it was. The key precedence is the resolver's, the same as sync's and the
102
+ * CLI's: the stored key for the link's host, else the exported one (which is how CI, with no
103
+ * keystore, gets here). An explicitly exported HOST still wins, so a proxy is not overridden.
103
104
  *
104
105
  * The credential arrives through `deps.linkedCloud`, a port, because resolving it here would make
105
106
  * the tool surface reach into `memory/cloud` — a reach the directory guard refused, correctly.
@@ -7,7 +7,7 @@
7
7
  * nothing grades) but the flows that now exist on disk, because every future run replays those
8
8
  * deterministically with no model in the loop at all.
9
9
  */
10
- import { ReticleEnv, ReticleTool, apiKeyFrom, asRecord } from '@reticlehq/core';
10
+ import { ReticleEnv, ReticleTool, cloudUrlFrom, asRecord } from '@reticlehq/core';
11
11
  import { projectForRoot } from '../../memory/project/project-for-root.js';
12
12
  import { harnessDriver, harnessOptionsFromEnv, } from '../../features/harness/driver.js';
13
13
  import { fillValues } from '../../features/harness/fill-values.js';
@@ -22,6 +22,7 @@ import { openAiDriver, openAiOptionsFromEnv } from '../../features/harness/opena
22
22
  import { fetchPlatformConfig } from '../../features/harness/platform-config.js';
23
23
  import { DEFAULT_MAX_STEPS, runHarness, } from '../../features/harness/harness.js';
24
24
  import { reticleToolset } from './harness-toolset.js';
25
+ import { secretEnvKey } from '../../language/flows/flows.js';
25
26
  /**
26
27
  * Why the harness is unavailable, phrased as the way to make it available.
27
28
  *
@@ -53,15 +54,14 @@ const msgUnknownDriver = (asked) => `Unknown harness driver \`${asked}\`. Known
53
54
  *
54
55
  * Resolved into the env rather than threaded through four call sites: the drivers and the
55
56
  * preference lookup all read an env record, and giving them a completed one leaves each of them
56
- * exactly as simple as it was. An EXPLICIT variable still wins — someone who exported a key meant
57
- * that key, and CI has no linked project to read.
57
+ * exactly as simple as it was. The key precedence is the resolver's, the same as sync's and the
58
+ * CLI's: the stored key for the link's host, else the exported one (which is how CI, with no
59
+ * keystore, gets here). An explicitly exported HOST still wins, so a proxy is not overridden.
58
60
  *
59
61
  * The credential arrives through `deps.linkedCloud`, a port, because resolving it here would make
60
62
  * the tool surface reach into `memory/cloud` — a reach the directory guard refused, correctly.
61
63
  */
62
64
  export async function withLinkedCredential(deps, env) {
63
- if (apiKeyFrom(env) !== undefined)
64
- return env;
65
65
  try {
66
66
  const linked = await deps.linkedCloud?.();
67
67
  if (linked === undefined || null === linked)
@@ -69,7 +69,7 @@ export async function withLinkedCredential(deps, env) {
69
69
  return {
70
70
  ...env,
71
71
  [ReticleEnv.API_KEY]: linked.apiKey,
72
- [ReticleEnv.CLOUD_URL]: env[ReticleEnv.CLOUD_URL] ?? linked.url,
72
+ [ReticleEnv.CLOUD_URL]: cloudUrlFrom(env) ?? linked.url,
73
73
  };
74
74
  }
75
75
  catch {
@@ -412,6 +412,7 @@ function buildDriver(env, maxSteps, plan, requested, fills) {
412
412
  * With no such key the label heuristic answers, exactly as it did before this existed.
413
413
  */
414
414
  fillValue: fillValues({
415
+ secret: (label) => env[secretEnvKey(label)],
415
416
  ...(fills === undefined ? {} : { cache: fills }),
416
417
  ...(harnessOptionsFromEnv(env) === undefined
417
418
  ? {}
@@ -21,7 +21,7 @@ import { hasAnyAppConnectedBefore } from '../../memory/recall/prior/connection-m
21
21
  import { AGENT_DRIVING_ELSEWHERE, AGENT_DRIVING_HERE_AGAIN, watchersToNotify, } from '../../portal/session/lease-visibility.js';
22
22
  import { reticleStateHome } from '../../command/daemon/daemon.js';
23
23
  import { LeaseNotReadyReason, REDACTED_VALUE, RETICLE_URL_PARAM, RETICLE_DEFAULT_PORT, ReticleCommand, SeedStorageSchema, scrubKnownSecrets, } from '@reticlehq/core';
24
- import { ReticleTool } from '@reticlehq/core';
24
+ import { ReticleTool, SESSION_HEALTH } from '@reticlehq/core';
25
25
  import { asString } from '@reticlehq/core';
26
26
  import { chromiumHint } from '../../command/cli/doctor/browser/chromium-hint.js';
27
27
  /**
@@ -165,6 +165,10 @@ export function scrubSeedFromError(text, seed) {
165
165
  }
166
166
  return out;
167
167
  }
168
+ /** True for a Playwright `storageState()` export (`{ origins: [...] }`), not our seed shape. */
169
+ function looksLikeStorageStateExport(seed) {
170
+ return Array.isArray(seed?.origins);
171
+ }
168
172
  /**
169
173
  * Turn a raw navigation failure (Playwright's `page.goto: net::ERR_… at <url>\nCall log:…`, often
170
174
  * with ANSI codes) into a short, clean reason — so the agent/user sees "is the app running?" instead
@@ -494,8 +498,13 @@ export const LEASE_ACQUIRE_TOOL = {
494
498
  if (seedStorageArg !== undefined) {
495
499
  const parsed = SeedStorageSchema.safeParse(seedStorageArg);
496
500
  if (!parsed.success) {
497
- const issuesMsg = scrubSeedFromError(parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join(', '), seedStorageArg);
498
- throw new Error(`reticle_lease{action:"acquire"} seedStorage is invalid: ${issuesMsg}`);
501
+ const msg = scrubSeedFromError(parsed.error.issues
502
+ .map((i) => (i.path.length > 0 ? `${i.path.join('.')}: ${i.message}` : i.message))
503
+ .join(', '), seedStorageArg);
504
+ const hint = looksLikeStorageStateExport(seedStorageArg)
505
+ ? 'This looks like a Playwright storageState() export ({ origins, cookies }); seedStorage takes { local?, session?, cookies? } (map origins[].localStorage into `local`).'
506
+ : 'Expected { local?, session?, cookies? }.';
507
+ throw new Error(`reticle_lease{action:"acquire"} seedStorage is invalid: ${msg}. ${hint}`);
499
508
  }
500
509
  validatedSeed = parsed.data;
501
510
  }
@@ -699,9 +708,7 @@ const PREFER_EXISTING_NOTE = 'a non-leased tab for this app was already connecte
699
708
  * is the right trade for a broadcast and the wrong one here, where a single `[0]` is handed back as
700
709
  * "drive this instead". With `projectId` undefined — and it is an OPTIONAL argument that an
701
710
  * acquiring agent has no reason to pass — every non-leased session qualified and the answer was
702
- * whichever the map yielded first. Measured against a real daemon: leasing `localhost:4312`
703
- * recommended a Phanpy tab on `:5173`, so an agent that complied would have driven a different
704
- * application and reported a verdict about it.
711
+ * whichever the map yielded first: leasing `localhost:4312` recommended a tab on `:5173`, another app.
705
712
  *
706
713
  * So this narrows by the one thing a lease always knows: the ORIGIN it is being taken against.
707
714
  * `projectId` still narrows further when given. Nothing is recommended when nothing matches, which
@@ -718,6 +725,8 @@ function liveTabFor(deps, projectId, url) {
718
725
  const candidates = deps.sessions
719
726
  .all()
720
727
  .filter((session) => session.url !== undefined && originOf(session.url) === wanted)
728
+ // Nor a tab gone silent: one last heard from 105s earlier was named here and answered nothing.
729
+ .filter((session) => (session.lastSeenMs?.() ?? 0) <= SESSION_HEALTH.STALE_THRESHOLD_MS)
721
730
  .map((session) => ({ id: session.id, projectId: session.projectId }));
722
731
  return watchersToNotify(candidates, pool.leasedSessionIds(), projectId)[0];
723
732
  }
@@ -38,7 +38,10 @@ const NO_TOOL_EQUIVALENT = [
38
38
  // \u2014 so the optional group could not match, the clause survived, and the replacement landed
39
39
  // inside the original quoting. What an agent read was the human-equivalent clause twice over,
40
40
  // with backticks nested three deep.
41
- pattern: /`?reticle_run\s*\{\s*tool:\s*\\?"reticle_lease\\?"[^}]*\}`?(\s*\([^)]*\))?(\s*[-\u2014]+\s*reticle_lease is[^.]*\.)?/g,
41
+ // The body allows ONE nested object, because the call is `{ tool, args: { action, url } }`: the
42
+ // hint used to put `action` and `url` beside `tool`, which reticle_run refuses, and a `[^}]*\}`
43
+ // body stopped at the inner brace and left ` }` behind once the hint was corrected.
44
+ pattern: /`?reticle_run\s*\{\s*tool:\s*\\?"reticle_lease\\?"(?:[^{}]|\{[^{}]*\})*\}`?(\s*\([^)]*\))?(\s*[-\u2014]+\s*reticle_lease is[^.]*\.)?/g,
42
45
  instead: 'the CLI: `reticle open <url>` (a human can equivalently run `reticle drive <url>`)',
43
46
  },
44
47
  {
@@ -33,6 +33,7 @@ import { withControl } from '../../portal/session/control-envelope.js';
33
33
  import { asNumber, asRecord, asString } from '@reticlehq/core';
34
34
  import { intentArg, sessionIdShape, commandOrThrow } from './tool-kit.js';
35
35
  import { gradeOfPredicate } from './assert/assert-grade.js';
36
+ import { captureAssertion } from '../../language/flows/replay.js';
36
37
  /**
37
38
  * Evidence-completeness block: present on observe/network/console only when the ring buffer has
38
39
  * evicted events, so a "no such event" answer is distinguishable from "I dropped it" (issue #27).
@@ -302,7 +303,7 @@ export const OBSERVE_TOOLS = [
302
303
  '{ net, urlContains|method|status|count|bodyMatches|bodyContains|requestBodyContains|requestBodyMatches } ' +
303
304
  '{ state, path|equals } { route, pathname (exact) | contains (path+query+hash) } ' +
304
305
  '{ element, testid|role|text } { text } { console, level|contains|absent } { animation, name } ' +
305
- '{ settled } — combine with { allOf | anyOf | not }. Prefer a signal/net/state consequence ' +
306
+ '{ settled } { compare, left, right } — combine with { allOf | anyOf | not }. Prefer a signal/net/state consequence ' +
306
307
  'over element/text presence.'),
307
308
  until: PredicateSchema.optional().describe("Alias for `predicate` (act_and_wait's name)."),
308
309
  timeout_ms: timeoutMsSchema
@@ -446,6 +447,13 @@ export const OBSERVE_TOOLS = [
446
447
  // destroys — see runs/run-context.ts. Recorded WITHOUT an attribution window: this tool drives
447
448
  // nothing, so no event it observed was caused by it.
448
449
  session.recordAction(ReticleTool.ASSERT, asRecord(args), verdictEffect);
450
+ // A passing check over the default window (since the last act) is proof of that act, so it
451
+ // joins that step in the recording — otherwise "act, then assert" saves a bare click. An
452
+ // explicit `since` may reach back past the last step, where replay would not find it. The
453
+ // store itself refuses when a navigation came after that step: see RecordingStore.markNavigated.
454
+ if (Verified.YES === decision['verified'] && args['since'] === undefined) {
455
+ captureAssertion(deps.recordings, predicate);
456
+ }
449
457
  return withControl(session, {
450
458
  ...decision,
451
459
  ...annotateStarvedFailure(session, verdict),
@@ -236,6 +236,10 @@ export const RAW_TOOLS = [
236
236
  .boolean()
237
237
  .optional()
238
238
  .describe('True when the page exceeded the snapshot cap. The tree is then a document-order PREFIX: an element being absent from it does NOT mean it is absent from the page.'),
239
+ // The refs a capped read never entered — re-read one with `scope`. Undeclared, a validating
240
+ // client stripped it, and a truncated tree arrived with no way to finish reading it.
241
+ unread: z.array(z.string()).optional(),
242
+ unreadOverflow: z.boolean().optional(),
239
243
  note: z
240
244
  .string()
241
245
  .optional()
@@ -19,8 +19,6 @@
19
19
  * never quietly start forwarding free text.
20
20
  */
21
21
  export declare const OTHER_VALUE = "other";
22
- /** True for anything we are willing to report a value for. */
23
- export declare function isSafeEnumParam(name: string): boolean;
24
22
  /**
25
23
  * The reportable form of one parameter: `name` on its own, or `name:value` when the parameter is a
26
24
  * known enum. Never the raw value of anything else.
@@ -41,10 +41,6 @@ const SAFE_ENUM_PARAMS = new Map([
41
41
  ['level', new Set(['error', 'warn', 'info', 'log', 'debug'])],
42
42
  ]);
43
43
  export const OTHER_VALUE = 'other';
44
- /** True for anything we are willing to report a value for. */
45
- export function isSafeEnumParam(name) {
46
- return SAFE_ENUM_PARAMS.has(name);
47
- }
48
44
  /**
49
45
  * The reportable form of one parameter: `name` on its own, or `name:value` when the parameter is a
50
46
  * known enum. Never the raw value of anything else.
@@ -2,9 +2,8 @@
2
2
  * Whose defect was that tool error?
3
3
  *
4
4
  * `toolErrors` was one number covering three failures with three different fixes, so the top-line
5
- * count could be acted on only by unpacking `errors[]` by hand and reading the prose. Measured over
6
- * 2026-08-10/11: 60 `state`, 22 `refusal`, 20 `schema` (serialized zod arrays) and 20 stale refs.
7
- * Read as "126 tool errors" that hides the sixth of them where OUR schema failed to explain itself —
5
+ * count could be acted on only by unpacking `errors[]` by hand and reading the prose. Read as one
6
+ * number, it hides the share where OUR schema failed to explain itself (serialized zod arrays),
8
7
  * which is the only bucket we can fix by writing better descriptions.
9
8
  *
10
9
  * Classified from the message SHAPE, never stored raw: this returns a bucket, and the caller keeps
@@ -2,9 +2,8 @@
2
2
  * Whose defect was that tool error?
3
3
  *
4
4
  * `toolErrors` was one number covering three failures with three different fixes, so the top-line
5
- * count could be acted on only by unpacking `errors[]` by hand and reading the prose. Measured over
6
- * 2026-08-10/11: 60 `state`, 22 `refusal`, 20 `schema` (serialized zod arrays) and 20 stale refs.
7
- * Read as "126 tool errors" that hides the sixth of them where OUR schema failed to explain itself —
5
+ * count could be acted on only by unpacking `errors[]` by hand and reading the prose. Read as one
6
+ * number, it hides the share where OUR schema failed to explain itself (serialized zod arrays),
8
7
  * which is the only bucket we can fix by writing better descriptions.
9
8
  *
10
9
  * Classified from the message SHAPE, never stored raw: this returns a bucket, and the caller keeps
@@ -2,5 +2,3 @@
2
2
  export declare function withHarnessDrive<T>(drive: () => Promise<T>): Promise<T>;
3
3
  /** True while a harness drive is in progress. */
4
4
  export declare function harnessDriving(): boolean;
5
- /** Tests only — a leaked span would mis-attribute every verdict after it. */
6
- export declare function resetHarnessDrive(): void;
@@ -33,7 +33,3 @@ export async function withHarnessDrive(drive) {
33
33
  export function harnessDriving() {
34
34
  return depth > 0;
35
35
  }
36
- /** Tests only — a leaked span would mis-attribute every verdict after it. */
37
- export function resetHarnessDrive() {
38
- depth = 0;
39
- }
@@ -1,5 +1,6 @@
1
1
  import { Bridge } from './portal/bridge/bridge.js';
2
2
  import { FlowStore } from './language/flows/flows.js';
3
+ import type { FlowFile } from '@reticlehq/core';
3
4
  import type { FileSystemPort } from './memory/project/fs/fs-port.js';
4
5
  import type { TapeStep } from './memory/journal/drive-flow.js';
5
6
  import type { OnboardingStep } from '@reticlehq/core/telemetry';
@@ -26,6 +27,8 @@ export declare function attachJournal(bridge: Bridge, deps: {
26
27
  /** Tell cloud sync a run landed, so it cycles instead of waiting for its timer. */
27
28
  onRunPersisted?: () => void;
28
29
  flows?: FlowStore;
30
+ /** Who is driving, stamped on each flow teardown saves. */
31
+ author?: () => FlowFile['author'];
29
32
  /** What this project is willing to keep — the `retain` block of its `.reticle.json`. */
30
33
  retain?: PruneWorkspaceOptions;
31
34
  }): void;
@@ -65,6 +65,10 @@ export function attachJournal(bridge, deps) {
65
65
  // Teardown: flush the journal tail to disk + persist what this session learned.
66
66
  bridge.attachSessionEnd(makeSessionEnd({
67
67
  ...deps,
68
+ // A drive is saved in the session's own project, where replay will look for it.
69
+ ...(deps.flows === undefined
70
+ ? {}
71
+ : { flowsAt: (root) => new FlowStore(deps.fs, root, { now: Date.now }) }),
68
72
  ...(deps.retain === undefined ? {} : { retain: deps.retain }),
69
73
  // Retention runs from teardown, and it must not delete the journal of a session that is still
70
74
  // being written. The registry is the only thing that knows which those are.
@@ -18,5 +18,3 @@
18
18
  * allowed to depend on anything, which is what makes them useful as fixtures.
19
19
  */
20
20
  export declare function publishedPackageDirs(): string[];
21
- /** The same list, as the last part of each path: `core`, `server`, `dom`, and so on. */
22
- export declare function publishedPackageNames(): string[];
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
- import { basename, join } from 'node:path';
2
+ import { join } from 'node:path';
3
3
  import { workspaceGlobs } from '../../scripts/check-boundaries.mjs';
4
4
  import { REPO_ROOT } from './machine/repo-root.js';
5
5
  /**
@@ -47,11 +47,3 @@ function collect(dir, levels, into) {
47
47
  collect(join(dir, entry.name), levels - 1, into);
48
48
  }
49
49
  }
50
- /** The same list, as the last part of each path: `core`, `server`, `dom`, and so on. */
51
- export function publishedPackageNames() {
52
- // `basename`, not `split('/')`: these directories are built with `join`, so on Windows they hold
53
- // backslashes and contain no forward slash at all — the split then returns the WHOLE ABSOLUTE
54
- // PATH as the package name. Every caller comparing a name against a real one silently stops
55
- // matching, which is the same defect that reported fourteen packages as uncovered.
56
- return publishedPackageDirs().map((dir) => basename(dir));
57
- }
package/docs/README.md CHANGED
@@ -13,8 +13,9 @@ Everything here is published to [docs.reticle.sh](https://docs.reticle.sh) by `d
13
13
  | Page | What it answers |
14
14
  | --- | --- |
15
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 |
16
+ | [what-is-reticle.mdx](what-is-reticle.mdx) | the definition, where it fits beside other tools, and what it needs to run |
17
+ | [why-reticle.mdx](why-reticle.mdx) | the false-green problem, the measured case, and what pairs well with it |
18
+ | [use-cases.mdx](use-cases.mdx) | what people verify with it: agent-built changes, security behaviour, accessibility, performance, SEO basics, personas |
18
19
  | [install-agentic.mdx](install-agentic.mdx) | what `npx @reticlehq/server init` writes, and how to read its marks |
19
20
  | [install-manual.mdx](install-manual.mdx) | wiring the MCP server and SDK by hand, per agent and framework |
20
21
  | [skill-setup.md](skill-setup.md) | the setup half of SKILL.md: what to pass `init`, reading its report, and what to do when it cannot finish |
@@ -24,6 +25,7 @@ Everything here is published to [docs.reticle.sh](https://docs.reticle.sh) by `d
24
25
  | [capabilities.mdx](capabilities.mdx) | coverage, storage, fake clocks, crawl, visual diff, network mocking |
25
26
  | [recipes.mdx](recipes.mdx) | worked examples for real situations, with the response each returned |
26
27
  | [actions.mdx](actions.mdx) | every action and its arguments, including `press` and its history |
28
+ | [environment.mdx](environment.mdx) | every environment variable Reticle reads, what it changes, and which process reads it |
27
29
  | [faq.mdx](faq.mdx) | production, frameworks, comparisons, and the honest limits |
28
30
  | [tools/overview.mdx](tools/overview.mdx) | the 18 advertised tools, the 30 in the cold tail, and why |
29
31
  | [tools/snapshot.mdx](tools/snapshot.mdx) | `reticle_look { action: "page" }`: three modes, from full tree to a 25-token route check |
package/docs/actions.mdx CHANGED
@@ -5,7 +5,7 @@ description: Every action reticle_act and reticle_act_and_wait can perform, with
5
5
  icon: hand-pointer
6
6
  ---
7
7
 
8
- Every action Reticle can perform takes the same shape: a `ref` from [`reticle_snapshot`](/tools/snapshot) or [`reticle_query`](/tools/query), an `action` name, and an optional `args` object. The same seventeen actions are available on [`reticle_act`](/tools/act), which proves nothing, and on [`reticle_act_and_wait`](/tools/act-and-wait), which returns a verdict.
8
+ Every action Reticle can perform takes the same shape: a `ref` from [`reticle_look { action: "page" }`](/tools/snapshot) or [`reticle_look { action: "find" }`](/tools/query), an `action` name, and an optional `args` object. The same seventeen actions are available on [`reticle_act`](/tools/act), which proves nothing, and on [`reticle_act_and_wait`](/tools/act-and-wait), which returns a verdict.
9
9
 
10
10
  ```json
11
11
  { "ref": "e104", "action": "fill", "args": { "value": "reticle" } }
@@ -20,7 +20,7 @@ Every action Reticle can perform takes the same shape: a `ref` from [`reticle_sn
20
20
  | `type` | `{ text }` | **Appends** to the current value |
21
21
  | `clear` | none | Empties an input |
22
22
  | `select` | `{ value }` | For `<select>` |
23
- | `check` / `uncheck` | none | Checkbox and radio |
23
+ | `check` / `uncheck` | none | Checkbox and radio, or a `role="checkbox"`/`role="switch"` element (fails if `aria-checked` does not change) |
24
24
  | `submit` | none | Submits the element's `<form>` |
25
25
  | `press` | `{ text }` | The key **name**: `Escape`, `Tab`, `Enter` |
26
26
  | `scrollIntoView` | none | |
@@ -93,7 +93,7 @@ Several actions in one round trip:
93
93
  ```
94
94
 
95
95
  <Warning>
96
- [`reticle_act_sequence`](/tools/act-sequence) returns `ok: true` when the steps were
96
+ [`reticle_act { steps: [...] }`](/tools/act-sequence) returns `ok: true` when the steps were
97
97
  **dispatched**, not when they worked. Batch the setup, then prove the outcome with
98
98
  [`reticle_act_and_wait`](/tools/act-and-wait) on the final step.
99
99
  </Warning>
@@ -4,7 +4,7 @@ description: 'One screen to get fluent: the look → act → observe → assert
4
4
  icon: bolt
5
5
  ---
6
6
 
7
- The loop is **look → act → observe → assert**, and only `reticle_act_and_wait` and `reticle_assert` produce a verdict. Reach for `reticle_act_and_wait({ ref, action, until })` first: it names the expected consequence before the action, which is the difference between a check and a rationalisation. `verified: "unknown"` is not a pass.
7
+ The loop is **look → act → observe → assert**, and verdicts come from `reticle_act_and_wait`, `reticle_assert`, and `reticle_act { steps }` when a step declares `expect`. Reach for `reticle_act_and_wait({ ref, action, until })` first: it names the expected consequence before the action, which is the difference between a check and a rationalisation. `verified: "unknown"` is not a pass.
8
8
 
9
9
  One screen to get fluent. Reticle is the **proof layer for AI agents**: no screenshots, no vision model, evidence not prose. Everything below returns structured data. Full guide: [usage.md](usage.md).
10
10
 
@@ -17,7 +17,7 @@ One screen to get fluent. Reticle is the **proof layer for AI agents**: no scree
17
17
  | **observe** | `reticle_observe` / `reticle_assert { action: "wait" }` | Everything the app did after `since` / block until true. |
18
18
  | **assert** | `reticle_assert` | Evaluate a predicate → `{ pass, evidence, failureReason? }`. The end of every loop. |
19
19
 
20
- > **Only `reticle_act_and_wait` and `reticle_assert` produce a verdict.** Everything else moves or reads the app and proves nothing, so a drive ending without one of those two has no result however many tools it used. `reticle_act` is the tool agents reach for by habit and it catches nothing; `act_and_wait` is where defects actually surface. And `verified: "unknown"` is not a pass: it means Reticle drove the app and could not tell what happened. Report it as unknown.
20
+ > **Verdicts come from `reticle_act_and_wait`, `reticle_assert`, and `reticle_act { steps }` when a step declares `expect`.** Everything else moves or reads the app and proves nothing, so a drive ending without a verdict has no result however many tools it used. `reticle_act` is the tool agents reach for by habit and it catches nothing; `act_and_wait` is where defects actually surface. And `verified: "unknown"` is not a pass: it means Reticle drove the app and could not tell what happened. Report it as unknown.
21
21
 
22
22
  `reticle_act` returns a `since` cursor; pass it to `reticle_observe({ since })` to scope the window. Elements are addressed by stable refs (`e7`) from `snapshot`/`query`; they re-resolve across re-renders.
23
23
 
@@ -125,9 +125,9 @@ Call an old name and you are told the new one. `reticle_tools` prints the live s
125
125
 
126
126
  - `reticle_verify`: **the only core tool that CONCLUDES.** `{ action:"crawl" }` drives every reachable control itself and reports the whole fault set in one call (destructive: it really clicks). `{ action:"flows" }` replays every saved flow with no model in the loop. Reach for it when you think you are done: every other tool here answers "here is more to look at".
127
127
 
128
- Not on this surface at all, and NOT reachable by any call from it: `reticle_capabilities` (the app's whole testable surface in one call), `reticle_domain` (learn the app + gaps), `reticle_baseline {action:"diff"}`, `reticle_project` (run history), `reticle_screenshot`, `reticle_visual_diff`, `reticle_storage`, `reticle_network_mock`, `reticle_clock`. They need a daemon started with `RETICLE_ADVERTISE_ALL_TOOLS=1`, which advertises the full table.
128
+ Not on this surface at all, but one hop away: `reticle_run { tool, args }` calls any registered tool by name, advertised or not, and `reticle_tools { names: [...] }` loads its arguments. That reaches `reticle_capabilities` (the app's whole testable surface in one call), `reticle_domain` (learn the app + gaps), `reticle_baseline {action:"diff"}`, `reticle_project` (run history), `reticle_screenshot`, `reticle_visual_diff`, `reticle_storage`, `reticle_network_mock` and `reticle_clock`. A daemon started with `RETICLE_ADVERTISE_ALL_TOOLS=1` advertises them directly instead.
129
129
 
130
- **What the wider surface adds, if you start the daemon with `RETICLE_ADVERTISE_ALL_TOOLS=1`:** record/replay a journey (`reticle_record {action:"start"}/stop`, `reticle_replay`), persist a self-healing golden flow (`reticle_flow_save*` / `reticle_flow_replay` / `reticle_verify { action: "heal" }`), compile annotations (`reticle_annotate`), explore autonomously (`reticle_explore` lists controls; `reticle_verify {action:"crawl"}` is now advertised directly, clicks them all and reports anomalies, and is **destructive**), reveal a virtualized off-screen row (`reticle_scroll_to`, for when `reticle_look { action: "find" }` finds nothing because a windowed list hasn't rendered it yet), visual-check (`reticle_screenshot` / `reticle_visual_diff`, pinned with `reticle_viewport` for reproducible baselines), test error/edge states by stubbing the network (`reticle_network_mock`: 500 / offline / delay, driven or leased), control time for toasts/debounces/auto-dismiss (`reticle_clock { freeze | advanceMs | reset }`), or work with a human (`reticle_session {action:"end"}` / `reticle_session {action:"resume"}` / `reticle_session {action:"messages"}`, and **`reticle_session {action:"review"}`** to drain + fix the bugs the human flagged from the panel).
130
+ **What the wider surface adds, through `reticle_run` or advertised directly with `RETICLE_ADVERTISE_ALL_TOOLS=1`:** record/replay a journey (`reticle_record {action:"start"}/stop`, `reticle_replay`), persist a self-healing golden flow (`reticle_flow_save*` / `reticle_flow_replay` / `reticle_verify { action: "heal" }`), compile annotations (`reticle_annotate`), explore autonomously (`reticle_explore` lists controls; `reticle_verify {action:"crawl"}` is now advertised directly, clicks them all and reports anomalies, and is **destructive**), reveal a virtualized off-screen row (`reticle_scroll_to`, for when `reticle_look { action: "find" }` finds nothing because a windowed list hasn't rendered it yet), visual-check (`reticle_screenshot` / `reticle_visual_diff`, pinned with `reticle_viewport` for reproducible baselines), test error/edge states by stubbing the network (`reticle_network_mock`: 500 / offline / delay, driven or leased), control time for toasts/debounces/auto-dismiss (`reticle_clock { freeze | advanceMs | reset }`), or work with a human (`reticle_session {action:"end"}` / `reticle_session {action:"resume"}` / `reticle_session {action:"messages"}`, and **`reticle_session {action:"review"}`** to drain + fix the bugs the human flagged from the panel).
131
131
 
132
132
  ## flows vs baselines vs project.json (the persistence layers)
133
133
 
@@ -14,7 +14,7 @@ Reticle has three moving parts: a dev-only **SDK** inside your page that instrum
14
14
 
15
15
  Your app, in dev, embeds a tiny **SDK** that instruments the page (DOM, network, console, routing, framework state) and opens a WebSocket to a local **bridge**. The bridge runs inside the Reticle **server**, which also exposes an **MCP server**, the standard protocol AI agents speak.
16
16
 
17
- Your coding agent calls MCP tools (`reticle_query`, `reticle_act`, `reticle_assert`, …); the server turns them into commands over the WebSocket; the SDK executes them in the page and streams back structured events. The agent thus **looks, acts, observes, and asserts** on the real running app, never on a screenshot.
17
+ Your coding agent calls MCP tools (`reticle_look { action: "find" }`, `reticle_act`, `reticle_assert`, …); the server turns them into commands over the WebSocket; the SDK executes them in the page and streams back structured events. The agent thus **looks, acts, observes, and asserts** on the real running app, never on a screenshot.
18
18
 
19
19
  ```mermaid
20
20
  flowchart LR
@@ -23,7 +23,7 @@ flowchart LR
23
23
  B["@reticlehq/browser<br/>SDK in your app<br/>(the DOM)"]
24
24
  D[(".reticle/<br/>flows · baselines<br/>runs · contract")]
25
25
 
26
- A -- "MCP (stdio/SSE)<br/>reticle_query / act / assert" --> S
26
+ A -- "MCP (stdio/SSE)<br/>reticle_look / act / assert" --> S
27
27
  S -- "tool results" --> A
28
28
  S <-- "WebSocket<br/>commands / events" --> B
29
29
  S <--> D
@@ -37,7 +37,7 @@ Reticle is a pnpm + Turborepo monorepo. The split is not cosmetic: each boundary
37
37
 
38
38
  | Package | Runs in | Responsibility | Hard rule |
39
39
  | --- | --- | --- | --- |
40
- | `@reticlehq/core` | both | The **wire contract**: every constant + zod schema crossing any boundary | Depends only on `zod` |
40
+ | `@reticlehq/core` | both | The **wire contract**: every constant + zod schema crossing any boundary | Depends only on `zod` and `open-verification` |
41
41
  | `@reticlehq/browser` | the browser | Instrument the page; execute commands; emit events | Never imports Node APIs |
42
42
  | `@reticlehq/server` | Node | The bridge, the MCP server, the `reticle` CLI, flow/run storage | Never imports DOM APIs |
43
43
  | `@reticlehq/react` | the browser | The SDK **kit** you install in a browser app: re-exports the browser sensor (so one install gives both `reticle` and `install`) and maps a DOM node → React component → source `file:line` | Core works without the source-mapping half |
@@ -59,8 +59,8 @@ The browser and server both import it; neither can invent a message the other do
59
59
 
60
60
  1. **Connect.** In dev, your app calls `reticle.connect({ session })`. The SDK opens a WebSocket to the bridge (`ws://localhost:4400/reticle` by default) and sends a `HELLO` carrying the session id, protocol version, and (if configured) a pairing token. Each browser tab uses `SESSION_AUTO` (a unique id), so multiple apps/tabs never collide.
61
61
  2. **Capture.** The SDK installs observers: a `MutationObserver` for DOM changes, wrappers around `fetch`/`XHR` for network, a console hook, a history hook for routing, and registries the app opts into (`registerStore`, `registerCapabilities`, `reticle.signal`). Events flow into a bounded **ring buffer**, so recent history is always available and memory is capped.
62
- 3. **Look / act.** The agent calls an MCP tool. `reticle_query` finds an element by role/text/testid/ component and returns a stable **ref**. `reticle_act` dispatches an action against that ref and returns an **effect** report (did it land, did the DOM mutate, did focus move…). The server sends the command over the WebSocket; the SDK runs it and replies.
63
- 4. **Observe.** After an action, the agent reads what happened with `reticle_network`, `reticle_console`, `reticle_state`, or the reaction digest from `reticle_act_and_wait`. The server pulls the relevant slice of the ring buffer (scoped to a cursor so stale events can't leak in) and returns a compact summary.
62
+ 3. **Look / act.** The agent calls an MCP tool. `reticle_look { action: "find" }` finds an element by role/text/testid/ component and returns a stable **ref**. `reticle_act` dispatches an action against that ref and returns an **effect** report (did it land, did the DOM mutate, did focus move…). The server sends the command over the WebSocket; the SDK runs it and replies.
63
+ 4. **Observe.** After an action, the agent reads what happened with `reticle_observe { action: "network" }`, `reticle_observe { action: "console" }`, `reticle_look { action: "state" }`, or the reaction digest from `reticle_act_and_wait`. The server pulls the relevant slice of the ring buffer (scoped to a cursor so stale events can't leak in) and returns a compact summary.
64
64
  5. **Assert.** `reticle_assert` (and a flow's declared `success`) evaluates a **predicate** over program truth (a network call that returned 200, a store value, a `signal` the app emitted), not just "an element exists." This is the difference between "looks done" and "is done."
65
65
 
66
66
  ---
@@ -87,7 +87,7 @@ The same verification runs over and over, every commit, every CI run. Reticle re
87
87
 
88
88
  ### 4. Dev-only, localhost-only, your app data stays local
89
89
 
90
- The SDK is tree-shaken out of production builds and connects only to a local bridge. The bridge binds to loopback by default; exposing it beyond localhost _requires_ a pairing token (the server refuses to bind a non-loopback host without one). Every environment variable that gates a security control is a single named constant, so a typo can't silently disable auth. Nothing from the app under test ever leaves your machine: no DOM, network, console, state, or source. The CLI reports anonymous, opt-out usage metrics only (a random id + event names like `invoke`/`session_start`; no code, no PII; see [telemetry](telemetry.md)); opt out with `reticle telemetry disable`, `RETICLE_TELEMETRY=0`, or `DO_NOT_TRACK=1`. The one thing that carries free text is feedback you or your agent deliberately send (`reticle feedback` / `reticle_feedback`). It is never collected passively, it is redacted before sending, and it is disabled separately with `RETICLE_FEEDBACK=0`.
90
+ The SDK is tree-shaken out of production builds and connects only to a local bridge. The bridge binds to loopback by default; exposing it beyond localhost _requires_ a pairing token (the server refuses to bind a non-loopback host without one). Every environment variable that gates a security control is a single named constant, so a typo can't silently disable auth. Nothing from the app under test ever leaves your machine: no DOM, network, console, state, or source. The CLI reports anonymous, opt-out usage metrics only (a random id + event names like `invoke`/`session_start`; no code, no PII; see [telemetry](telemetry.md)); opt out with `reticle telemetry disable`, `RETICLE_TELEMETRY=0`, or `DO_NOT_TRACK=1`. The one thing that carries free text is feedback you or your agent deliberately send (`reticle feedback` / `reticle_session { action: "feedback" }`). It is never collected passively, it is redacted before sending, and it is disabled separately with `RETICLE_FEEDBACK=0`.
91
91
 
92
92
  ---
93
93