@reticlehq/server 3.3.0 → 3.5.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 (326) 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 +6 -2
  15. package/dist/command/cli/auth/cloud-session.js +2 -0
  16. package/dist/command/cli/auth/device-flow.d.ts +2 -2
  17. package/dist/command/cli/cli-doctor.js +7 -5
  18. package/dist/command/cli/cli-flow-commands.d.ts +3 -6
  19. package/dist/command/cli/cli-flow-commands.js +67 -4
  20. package/dist/command/cli/cli-parse.d.ts +8 -1
  21. package/dist/command/cli/cli-parse.js +24 -4
  22. package/dist/command/cli/cli-update-commands.js +8 -1
  23. package/dist/command/cli/cli-verify.d.ts +24 -3
  24. package/dist/command/cli/cli-verify.js +54 -11
  25. package/dist/command/cli/cloud-cli.js +64 -2
  26. package/dist/command/cli/cloud-kit.js +3 -5
  27. package/dist/command/cli/cloud-login.d.ts +1 -1
  28. package/dist/command/cli/cloud-login.js +27 -8
  29. package/dist/command/cli/demo/demo-page.js +6 -3
  30. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +5 -0
  31. package/dist/command/cli/doctor/browser/chromium-hint.js +5 -0
  32. package/dist/command/cli/launch/cli-launch.d.ts +8 -0
  33. package/dist/command/cli/launch/cli-launch.js +21 -9
  34. package/dist/command/cli/launch/sdk-version-match.d.ts +84 -0
  35. package/dist/command/cli/launch/sdk-version-match.js +206 -0
  36. package/dist/command/cli/lifecycle/daemon-lifecycle.js +2 -16
  37. package/dist/command/cli/ports/resolve/cli-port.d.ts +25 -0
  38. package/dist/command/cli/ports/resolve/cli-port.js +125 -15
  39. package/dist/command/cli/setup-mcp-cli.js +21 -13
  40. package/dist/command/cli/tutorial.d.ts +2 -2
  41. package/dist/command/cli/tutorial.js +21 -7
  42. package/dist/command/cli.js +79 -11
  43. package/dist/command/daemon/binding/daemon-status-probe.d.ts +14 -2
  44. package/dist/command/daemon/binding/daemon-status-probe.js +34 -6
  45. package/dist/command/daemon/daemon-resolve.d.ts +9 -0
  46. package/dist/command/daemon/daemon-resolve.js +1 -1
  47. package/dist/command/daemon/lifetime/daemon-usefulness.d.ts +7 -1
  48. package/dist/command/daemon/lifetime/daemon-usefulness.js +30 -7
  49. package/dist/command/dev/stale-issue-guard.d.ts +3 -4
  50. package/dist/command/dev/stale-issue-guard.js +8 -8
  51. package/dist/command/setup/bringup/connect-lease.d.ts +17 -0
  52. package/dist/command/setup/bringup/connect-lease.js +38 -0
  53. package/dist/command/setup/bringup/dev-server-files.d.ts +68 -0
  54. package/dist/command/setup/bringup/dev-server-files.js +131 -0
  55. package/dist/command/setup/bringup/dev-server-log-cap.d.ts +35 -0
  56. package/dist/command/setup/bringup/dev-server-log-cap.js +121 -0
  57. package/dist/command/setup/bringup/dev-server-wait.d.ts +2 -0
  58. package/dist/command/setup/bringup/dev-server-wait.js +1 -1
  59. package/dist/command/setup/bringup/ensure-daemon.d.ts +65 -4
  60. package/dist/command/setup/bringup/ensure-daemon.js +135 -16
  61. package/dist/command/setup/bringup/static-page-server.d.ts +35 -0
  62. package/dist/command/setup/bringup/static-page-server.js +151 -0
  63. package/dist/command/setup/desktop-shape.d.ts +4 -0
  64. package/dist/command/setup/desktop-shape.js +18 -2
  65. package/dist/command/setup/init/init-port.d.ts +15 -0
  66. package/dist/command/setup/init/init-port.js +44 -0
  67. package/dist/command/setup/init/init-runtime.d.ts +1 -1
  68. package/dist/command/setup/init/init-runtime.js +91 -9
  69. package/dist/command/setup/node-effects.d.ts +27 -14
  70. package/dist/command/setup/node-effects.js +86 -45
  71. package/dist/command/setup/probe/loopback-probe-urls.d.ts +7 -0
  72. package/dist/command/setup/probe/loopback-probe-urls.js +40 -0
  73. package/dist/command/setup/probe/page-probe.d.ts +13 -2
  74. package/dist/command/setup/probe/page-probe.js +18 -3
  75. package/dist/command/setup/run-setup.d.ts +28 -1
  76. package/dist/command/setup/run-setup.js +103 -15
  77. package/dist/command/setup/session-pick.d.ts +13 -0
  78. package/dist/command/setup/session-pick.js +20 -0
  79. package/dist/command/setup/setup-command.d.ts +2 -14
  80. package/dist/command/setup/setup-command.js +105 -31
  81. package/dist/command/setup/setup-mcp.d.ts +2 -0
  82. package/dist/command/setup/setup-mcp.js +17 -8
  83. package/dist/command/setup/terminal/wait-progress.d.ts +8 -0
  84. package/dist/command/setup/terminal/wait-progress.js +17 -0
  85. package/dist/command/version/peer-announce.d.ts +6 -2
  86. package/dist/command/version/peer-announce.js +8 -6
  87. package/dist/command/version/version-nudge.d.ts +17 -3
  88. package/dist/command/version/version-nudge.js +25 -4
  89. package/dist/features/crawl/crawl-empty.js +1 -1
  90. package/dist/features/crawl/crawl-tools.d.ts +6 -0
  91. package/dist/features/crawl/crawl-tools.js +57 -0
  92. package/dist/features/crawl/crawl.js +171 -157
  93. package/dist/features/exhaust/app-coverage.d.ts +33 -0
  94. package/dist/features/exhaust/app-coverage.js +31 -0
  95. package/dist/features/exhaust/code-coverage.d.ts +47 -0
  96. package/dist/features/exhaust/code-coverage.js +77 -0
  97. package/dist/features/exhaust/exhaust-run.d.ts +33 -0
  98. package/dist/features/exhaust/exhaust-run.js +82 -0
  99. package/dist/features/exhaust/explorer.d.ts +103 -0
  100. package/dist/features/exhaust/explorer.js +195 -0
  101. package/dist/features/exhaust/failure-branches.d.ts +33 -0
  102. package/dist/features/exhaust/failure-branches.js +92 -0
  103. package/dist/features/exhaust/ledger.d.ts +202 -0
  104. package/dist/features/exhaust/ledger.js +252 -0
  105. package/dist/features/exhaust/session-fold.d.ts +31 -0
  106. package/dist/features/exhaust/session-fold.js +79 -0
  107. package/dist/features/exhaust/session-port.d.ts +36 -0
  108. package/dist/features/exhaust/session-port.js +102 -0
  109. package/dist/features/harness/drive-report.d.ts +0 -9
  110. package/dist/features/harness/drive-report.js +18 -0
  111. package/dist/features/harness/driver.js +4 -7
  112. package/dist/features/harness/fill-values.d.ts +10 -0
  113. package/dist/features/harness/fill-values.js +50 -11
  114. package/dist/features/harness/jev-driver.js +30 -13
  115. package/dist/features/harness/openai-driver.d.ts +12 -0
  116. package/dist/features/harness/openai-driver.js +21 -12
  117. package/dist/features/harness/platform-config.js +5 -6
  118. package/dist/features/license/license.d.ts +2 -2
  119. package/dist/index.d.ts +9 -0
  120. package/dist/index.js +56 -19
  121. package/dist/judgement/capsule/capsule-store.d.ts +4 -4
  122. package/dist/judgement/domain/domain-model.js +3 -1
  123. package/dist/judgement/oracles/self-instrument.js +4 -2
  124. package/dist/judgement/runs/artifact/replay-mapping.d.ts +2 -2
  125. package/dist/judgement/runs/artifact/replay-mapping.js +44 -9
  126. package/dist/judgement/runs/verification-sync.d.ts +3 -1
  127. package/dist/judgement/runs/verification-sync.js +1 -1
  128. package/dist/language/flows/change/affected.d.ts +9 -0
  129. package/dist/language/flows/change/affected.js +35 -0
  130. package/dist/language/flows/change/gate.d.ts +20 -0
  131. package/dist/language/flows/change/gate.js +12 -1
  132. package/dist/language/flows/flow-author.d.ts +7 -0
  133. package/dist/language/flows/flow-author.js +19 -0
  134. package/dist/language/flows/flow-expect-grammar.js +1 -1
  135. package/dist/language/flows/flow-journey.d.ts +54 -0
  136. package/dist/language/flows/flow-journey.js +119 -0
  137. package/dist/language/flows/flow-replay-run.d.ts +2 -3
  138. package/dist/language/flows/flow-replay-run.js +13 -47
  139. package/dist/language/flows/flow-replay.d.ts +22 -8
  140. package/dist/language/flows/flow-replay.js +55 -9
  141. package/dist/language/flows/flow-result.d.ts +2 -0
  142. package/dist/language/flows/flow-result.js +25 -0
  143. package/dist/language/flows/flow-tools.js +17 -5
  144. package/dist/language/flows/flows.d.ts +10 -1
  145. package/dist/language/flows/flows.js +90 -14
  146. package/dist/language/flows/recording/replay-halt.js +3 -1
  147. package/dist/language/flows/recording/tape/recordings.d.ts +30 -1
  148. package/dist/language/flows/recording/tape/recordings.js +55 -1
  149. package/dist/language/flows/replay.d.ts +19 -0
  150. package/dist/language/flows/replay.js +42 -6
  151. package/dist/language/flows/verify-change-tools.js +5 -2
  152. package/dist/launch-chromium.d.ts +80 -0
  153. package/dist/launch-chromium.js +218 -0
  154. package/dist/memory/cloud/cloud-config.d.ts +11 -2
  155. package/dist/memory/cloud/cloud-config.js +47 -7
  156. package/dist/memory/cloud/cloud-sync.d.ts +4 -1
  157. package/dist/memory/cloud/cloud-sync.js +6 -9
  158. package/dist/memory/cloud/harness-offer.js +5 -6
  159. package/dist/memory/cloud/harness-switch.js +5 -6
  160. package/dist/memory/cloud/sync-cycle.d.ts +77 -3
  161. package/dist/memory/cloud/sync-cycle.js +325 -55
  162. package/dist/memory/cloud/sync-daemon.js +24 -20
  163. package/dist/memory/cloud/sync-disk.js +16 -2
  164. package/dist/memory/intent/intent-shard.d.ts +10 -32
  165. package/dist/memory/intent/intent-shard.js +0 -10
  166. package/dist/memory/intent/intent-tools.js +8 -3
  167. package/dist/memory/journal/drive-flow.d.ts +18 -13
  168. package/dist/memory/journal/drive-flow.js +45 -37
  169. package/dist/memory/journal/envelope.d.ts +8 -8
  170. package/dist/memory/journal/journal-query.d.ts +14 -0
  171. package/dist/memory/journal/journal-query.js +15 -0
  172. package/dist/memory/journal/on-disk/workspace-tiers.js +2 -0
  173. package/dist/memory/journal/session-end.d.ts +9 -1
  174. package/dist/memory/journal/session-end.js +24 -12
  175. package/dist/memory/project/artifact-root-resolver.d.ts +10 -0
  176. package/dist/memory/project/artifact-root-resolver.js +46 -19
  177. package/dist/memory/project/dir/reticle-dir.d.ts +2 -0
  178. package/dist/memory/project/dir/reticle-dir.js +1 -0
  179. package/dist/memory/project/project-tools.js +3 -1
  180. package/dist/portal/bridge/auth-failure-reason.d.ts +1 -1
  181. package/dist/portal/bridge/auth-failure-reason.js +4 -2
  182. package/dist/portal/bridge/bridge.d.ts +12 -3
  183. package/dist/portal/bridge/bridge.js +49 -3
  184. package/dist/portal/input/js-coverage.d.ts +25 -0
  185. package/dist/portal/input/js-coverage.js +30 -0
  186. package/dist/portal/input/real-input.d.ts +11 -0
  187. package/dist/portal/input/real-input.js +29 -4
  188. package/dist/portal/input/viewport-tools.js +16 -6
  189. package/dist/portal/pool/playwright-launcher.d.ts +6 -0
  190. package/dist/portal/pool/playwright-launcher.js +20 -13
  191. package/dist/portal/session/dev-server/dev-command.d.ts +2 -2
  192. package/dist/portal/session/dev-server/dev-command.js +30 -12
  193. package/dist/portal/session/dev-server/dev-server-probe.js +17 -1
  194. package/dist/portal/session/dev-server/route-status-probe.d.ts +24 -0
  195. package/dist/portal/session/dev-server/route-status-probe.js +57 -0
  196. package/dist/portal/session/facts/observed-state.d.ts +11 -0
  197. package/dist/portal/session/facts/observed-state.js +34 -0
  198. package/dist/portal/session/no-session-diagnosis.d.ts +29 -0
  199. package/dist/portal/session/no-session-diagnosis.js +62 -5
  200. package/dist/portal/session/no-session-next-action.d.ts +13 -0
  201. package/dist/portal/session/no-session-next-action.js +48 -9
  202. package/dist/portal/session/no-session-watch.d.ts +18 -0
  203. package/dist/portal/session/no-session-watch.js +88 -2
  204. package/dist/portal/session/session-health.js +3 -2
  205. package/dist/portal/session/session-manager.d.ts +20 -6
  206. package/dist/portal/session/session-manager.js +19 -3
  207. package/dist/portal/session/session-reaper.d.ts +28 -0
  208. package/dist/portal/session/session-reaper.js +49 -1
  209. package/dist/portal/session/session.d.ts +10 -8
  210. package/dist/portal/session/session.js +24 -32
  211. package/dist/surface/http-server.d.ts +3 -2
  212. package/dist/surface/http-server.js +8 -5
  213. package/dist/surface/mcp/mcp-proxy.d.ts +2 -2
  214. package/dist/surface/mcp/mcp-proxy.js +2 -2
  215. package/dist/surface/mcp/mcp.js +5 -3
  216. package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +13 -1
  217. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +27 -2
  218. package/dist/surface/mcp/proxy/proxy-log.js +3 -5
  219. package/dist/surface/mcp/server-instructions.js +9 -7
  220. package/dist/surface/tools/act/after-match.d.ts +17 -0
  221. package/dist/surface/tools/act/after-match.js +30 -0
  222. package/dist/surface/tools/act/durable.d.ts +69 -0
  223. package/dist/surface/tools/act/durable.js +139 -0
  224. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +13 -6
  225. package/dist/surface/tools/act/navigation/navigate-arrival.js +47 -10
  226. package/dist/surface/tools/act/navigation/navigate-result.d.ts +1 -12
  227. package/dist/surface/tools/act/navigation/navigate-result.js +21 -3
  228. package/dist/surface/tools/act/proved-controls.d.ts +9 -0
  229. package/dist/surface/tools/act/proved-controls.js +11 -0
  230. package/dist/surface/tools/act/sequence-grade.d.ts +5 -0
  231. package/dist/surface/tools/act/sequence-grade.js +32 -1
  232. package/dist/surface/tools/act-capsule.d.ts +7 -0
  233. package/dist/surface/tools/act-capsule.js +46 -1
  234. package/dist/surface/tools/act-sequence-tool.js +25 -8
  235. package/dist/surface/tools/act-tools.js +28 -52
  236. package/dist/surface/tools/assert/assert-grade.js +12 -1
  237. package/dist/surface/tools/assert/assert-verdict.js +2 -2
  238. package/dist/surface/tools/browser-tools.js +14 -2
  239. package/dist/surface/tools/coverage-tools.js +27 -1
  240. package/dist/surface/tools/dynamic-tools.js +29 -2
  241. package/dist/surface/tools/error-recovery.js +5 -6
  242. package/dist/surface/tools/feedback-invite.js +6 -4
  243. package/dist/surface/tools/harness-explore.d.ts +3 -2
  244. package/dist/surface/tools/harness-explore.js +12 -7
  245. package/dist/surface/tools/invoke-tool.js +12 -8
  246. package/dist/surface/tools/lease-tools.js +15 -6
  247. package/dist/surface/tools/live-call-text.js +4 -1
  248. package/dist/surface/tools/observe-tools.js +9 -1
  249. package/dist/surface/tools/tool-kit.d.ts +10 -0
  250. package/dist/surface/tools/tools.js +4 -0
  251. package/dist/telemetry/argument-shape.d.ts +0 -2
  252. package/dist/telemetry/argument-shape.js +1 -5
  253. package/dist/telemetry/error-class.d.ts +2 -3
  254. package/dist/telemetry/error-class.js +2 -3
  255. package/dist/telemetry/harness-drive.d.ts +0 -2
  256. package/dist/telemetry/harness-drive.js +0 -4
  257. package/dist/telemetry/onboarding-firsts.d.ts +2 -2
  258. package/dist/telemetry/onboarding-firsts.js +2 -2
  259. package/dist/wire-journal.d.ts +3 -0
  260. package/dist/wire-journal.js +4 -0
  261. package/dist/workspace-packages.d.ts +0 -2
  262. package/dist/workspace-packages.js +1 -9
  263. package/docs/README.md +4 -2
  264. package/docs/actions.mdx +3 -4
  265. package/docs/agent-cheatsheet.md +4 -4
  266. package/docs/architecture.md +6 -6
  267. package/docs/best-practices.mdx +1 -1
  268. package/docs/capabilities.mdx +32 -1
  269. package/docs/cli/cloud.mdx +20 -8
  270. package/docs/cli/doctor.mdx +4 -0
  271. package/docs/cli/feedback.mdx +2 -2
  272. package/docs/cli/gate.mdx +9 -4
  273. package/docs/cli/update.mdx +10 -0
  274. package/docs/cli.mdx +22 -7
  275. package/docs/desktop.mdx +14 -10
  276. package/docs/environment.mdx +87 -0
  277. package/docs/faq.mdx +18 -16
  278. package/docs/first-drive.md +5 -13
  279. package/docs/flows.md +21 -2
  280. package/docs/frameworks.mdx +29 -10
  281. package/docs/getting-started.md +15 -5
  282. package/docs/http-transport.md +6 -3
  283. package/docs/human-control.md +2 -2
  284. package/docs/index.mdx +16 -6
  285. package/docs/install-agentic.mdx +2 -2
  286. package/docs/install-manual.mdx +1 -1
  287. package/docs/integration-patterns.md +3 -3
  288. package/docs/multi-agent-testing.md +1 -1
  289. package/docs/packages/babel-plugin.mdx +1 -1
  290. package/docs/packages/browser.mdx +2 -2
  291. package/docs/packages/next.mdx +3 -0
  292. package/docs/packages/react.mdx +2 -2
  293. package/docs/packages/tauri.mdx +1 -1
  294. package/docs/packages/vite-plugin.mdx +4 -2
  295. package/docs/packages.mdx +9 -9
  296. package/docs/predicates.mdx +50 -5
  297. package/docs/quickstart.mdx +12 -10
  298. package/docs/recipes.mdx +2 -2
  299. package/docs/state-management.mdx +2 -2
  300. package/docs/system-map.md +1 -1
  301. package/docs/telemetry.md +1 -1
  302. package/docs/token-efficiency.md +3 -3
  303. package/docs/tools/act-and-wait.mdx +1 -0
  304. package/docs/tools/act-sequence.mdx +1 -1
  305. package/docs/tools/act.mdx +1 -1
  306. package/docs/tools/console.mdx +1 -1
  307. package/docs/tools/inspect.mdx +1 -1
  308. package/docs/tools/network.mdx +1 -1
  309. package/docs/tools/query.mdx +1 -1
  310. package/docs/tools/sessions.mdx +1 -1
  311. package/docs/tools/snapshot.mdx +1 -1
  312. package/docs/tools/state.mdx +1 -1
  313. package/docs/tools/wait-for.mdx +1 -1
  314. package/docs/usage.md +7 -7
  315. package/docs/use-cases.mdx +157 -0
  316. package/docs/vs-screenshots.mdx +3 -3
  317. package/docs/what-is-recorded.md +2 -2
  318. package/docs/what-is-reticle.mdx +17 -20
  319. package/docs/why-reticle.mdx +14 -21
  320. package/package.json +10 -9
  321. package/dist/judgement/capsule/minimize.d.ts +0 -12
  322. package/dist/judgement/capsule/minimize.js +0 -22
  323. package/dist/judgement/phenomena/phenomena.d.ts +0 -27
  324. package/dist/judgement/phenomena/phenomena.js +0 -101
  325. package/dist/language/flows/flow-report.d.ts +0 -22
  326. package/dist/language/flows/flow-report.js +0 -106
@@ -27,7 +27,9 @@ export function gradeSequence(steps, plan = { planned: steps.length, dispatched:
27
27
  const total = Math.max(plan.planned, steps.length);
28
28
  const declaring = steps.filter((step) => step.declared);
29
29
  const declared = declaring.length;
30
- const missed = declaring.find((step) => true !== step.held);
30
+ // `held: false` on an undeclared step means it could not be performed at all — the journey broke
31
+ // there whether or not anything was declared about it.
32
+ const missed = steps.find((step) => (step.declared ? true !== step.held : false === step.held));
31
33
  // A miss decides the grade before coverage does. How much of the plan declared nothing is
32
34
  // context on a red, never a softening of it.
33
35
  if (missed !== undefined) {
@@ -39,6 +41,17 @@ export function gradeSequence(steps, plan = { planned: steps.length, dispatched:
39
41
  because: `a declared consequence did not hold — observed ${observed}`,
40
42
  };
41
43
  }
44
+ // Same rule as a single action: an observed contradiction outranks a consequence that held.
45
+ const contradictedAt = steps.findIndex((step) => (step.contradicted ?? []).length > 0);
46
+ if (contradictedAt >= 0) {
47
+ const kinds = (steps[contradictedAt]?.contradicted ?? []).join(', ');
48
+ return {
49
+ verified: Verified.NO,
50
+ declared,
51
+ total,
52
+ because: `step ${String(contradictedAt + 1)}: channels disagree about this action (${kinds}) even though it went on`,
53
+ };
54
+ }
42
55
  /*
43
56
  * `unknown` rather than `no-fault`, and the distinction is load-bearing.
44
57
  *
@@ -68,6 +81,24 @@ export function gradeSequence(steps, plan = { planned: steps.length, dispatched:
68
81
  : `nothing was dispatched, so none of the ${String(total)} step(s) ran — read the per-step \`error\` for the one that was refused, and fix that before adding an \`expect\``,
69
82
  };
70
83
  }
84
+ // A journey is proved at its END. Declaring only early steps checks that it started, and a yes
85
+ // there was the one a single act_and_wait on the last step would never have given.
86
+ if (steps.length < total) {
87
+ return {
88
+ verified: Verified.UNKNOWN,
89
+ declared,
90
+ total,
91
+ because: `only ${String(steps.length)} of ${String(total)} steps ran, so the journey never reached its end`,
92
+ };
93
+ }
94
+ if (true !== steps.at(-1)?.declared) {
95
+ return {
96
+ verified: Verified.UNKNOWN,
97
+ declared,
98
+ total,
99
+ because: `the last step, where the journey ends, declared nothing — give it an \`expect\` naming the end state (${String(declared)} of ${String(total)} steps declared one)`,
100
+ };
101
+ }
71
102
  return {
72
103
  verified: Verified.YES,
73
104
  declared,
@@ -28,6 +28,13 @@ interface CapsuleSaveInputs {
28
28
  */
29
29
  root?: string | undefined;
30
30
  }
31
+ /**
32
+ * What the act claimed would happen, in words: `route /settings`, `POST /v1/pay → 200`,
33
+ * `element "Issues"`. Read off the predicate the agent declared, because the causal links only name
34
+ * signals and requests: a failed route, element or text claim used to be filed as "declared
35
+ * consequence", which tells whoever reads the capsule nothing about what was expected.
36
+ */
37
+ export declare function describeExpected(predicate: unknown): string | undefined;
31
38
  /** Returns the capsule id when one was written, or undefined when there was nothing to save. */
32
39
  export declare function saveFailedAssertCapsule(inputs: CapsuleSaveInputs): Promise<string | undefined>;
33
40
  export {};
@@ -14,6 +14,50 @@ import { CapsuleStore, capsuleFingerprint, capsuleId, CAPSULE_VERSION, } from '.
14
14
  import { ReticleTool } from '@reticlehq/core';
15
15
  import { asRecord, asString } from '@reticlehq/core';
16
16
  import { sessionRoot } from '../../memory/project/session-root.js';
17
+ /**
18
+ * What the act claimed would happen, in words: `route /settings`, `POST /v1/pay → 200`,
19
+ * `element "Issues"`. Read off the predicate the agent declared, because the causal links only name
20
+ * signals and requests: a failed route, element or text claim used to be filed as "declared
21
+ * consequence", which tells whoever reads the capsule nothing about what was expected.
22
+ */
23
+ export function describeExpected(predicate) {
24
+ const p = asRecord(predicate);
25
+ const kind = asString(p['kind']);
26
+ if (kind === undefined)
27
+ return undefined;
28
+ const all = (key, joiner) => {
29
+ const parts = (Array.isArray(p[key]) ? p[key] : [])
30
+ .map(describeExpected)
31
+ .filter((d) => d !== undefined);
32
+ return 0 === parts.length ? undefined : parts.join(joiner);
33
+ };
34
+ const q = asRecord(p['query']);
35
+ const named = asString(p['name']) ?? asString(q['name']) ?? asString(q['value']) ?? asString(q['testid']);
36
+ switch (kind) {
37
+ case 'route':
38
+ return `route ${asString(p['pathname']) ?? asString(p['contains']) ?? '?'}`;
39
+ case 'net': {
40
+ const status = 'number' === typeof p['status'] ? ` → ${String(p['status'])}` : '';
41
+ return `${asString(p['method']) ?? 'a'} ${asString(p['urlContains']) ?? 'request'}${status}`;
42
+ }
43
+ case 'signal':
44
+ return `signal ${asString(p['name']) ?? '?'}`;
45
+ case 'element':
46
+ return `${true === p['absent'] ? 'no ' : ''}element ${named === undefined ? '?' : `"${named}"`}`;
47
+ case 'text':
48
+ return `${true === p['absent'] ? 'no ' : ''}text "${asString(p['contains']) ?? '?'}"`;
49
+ case 'allOf':
50
+ return all('predicates', ' AND ');
51
+ case 'anyOf':
52
+ return all('predicates', ' OR ');
53
+ case 'not': {
54
+ const inner = describeExpected(p['predicate']);
55
+ return inner === undefined ? undefined : `not ${inner}`;
56
+ }
57
+ default:
58
+ return kind;
59
+ }
60
+ }
17
61
  /** Returns the capsule id when one was written, or undefined when there was nothing to save. */
18
62
  export async function saveFailedAssertCapsule(inputs) {
19
63
  const { deps, verdict, capsule, links, args, actResult, actedSource, root: given } = inputs;
@@ -47,7 +91,8 @@ export async function saveFailedAssertCapsule(inputs) {
47
91
  const body = {
48
92
  version: CAPSULE_VERSION,
49
93
  origin: 'failed-assert',
50
- expected: expectedText.length > 0 ? expectedText : 'declared consequence',
94
+ expected: describeExpected(args['until'] ?? args['predicate']) ??
95
+ (expectedText.length > 0 ? expectedText : 'declared consequence'),
51
96
  observed: capsule.firstDivergence?.observed ?? verdict.failureReason ?? 'not observed',
52
97
  steps: [
53
98
  {
@@ -11,12 +11,12 @@
11
11
  */
12
12
  import { z } from 'zod';
13
13
  import { timeoutMsSchema } from './args/numeric-bounds.js';
14
- import { compileSequenceStep } from '../../language/flows/replay.js';
14
+ import { compileSequenceStep, pathOf } from '../../language/flows/replay.js';
15
15
  import { sequenceStepArgs } from './act/act-preflight.js';
16
16
  import { ReticleTool } from '@reticlehq/core';
17
17
  import { healthEnvelope } from '../../portal/session/session-health.js';
18
18
  import { pausedShortCircuit, pausedOutputShape, withControl, } from '../../portal/session/control-envelope.js';
19
- import { asRecord } from '@reticlehq/core';
19
+ import { asRecord, isAbsenceDerived, isAdvisory } from '@reticlehq/core';
20
20
  import { sessionIdFromArgs } from './tools-helpers.js';
21
21
  import { describeStepResult, runStepWithStaleRetry } from './act/act-sequence-retry.js';
22
22
  import { assertSequenceSteps } from './act/act-preflight.js';
@@ -59,10 +59,9 @@ function effectOf(session, since) {
59
59
  const BURST_MAX_GAP_MS = 500;
60
60
  export const ACT_SEQUENCE_TOOL = {
61
61
  name: ReticleTool.ACT_SEQUENCE,
62
- // The example is required for a core tool, and this one carries weight: the measured loop it
63
- // replaces is literally a login form driven as three separate reticle_act calls (98 clicks and
64
- // 21 fills inside looping sessions, 2026-08-10/11). Showing fill -> fill -> click is showing the
65
- // exact shape an agent otherwise spends three round trips on.
62
+ // The example is required for a core tool, and this one carries weight: the loop it replaces is
63
+ // a login form driven as three separate reticle_act calls. Showing fill -> fill -> click is showing
64
+ // the exact shape an agent otherwise spends three round trips on.
66
65
  example: {
67
66
  steps: [
68
67
  { ref: 'e12', action: 'fill', args: { value: 'a@b.com' } },
@@ -133,6 +132,7 @@ export const ACT_SEQUENCE_TOOL = {
133
132
  if (paused !== undefined)
134
133
  return paused;
135
134
  const since = session.elapsed();
135
+ const pageBefore = pathOf(session.url);
136
136
  session.beginAction(ReticleTool.ACT_SEQUENCE, asRecord(args));
137
137
  try {
138
138
  const inputSteps = Array.isArray(args['steps']) ? args['steps'] : [];
@@ -230,8 +230,12 @@ export const ACT_SEQUENCE_TOOL = {
230
230
  * satisfy step four's assertion, which is a false green built out of correct parts.
231
231
  */
232
232
  const parsed = PredicateSchema.safeParse(step['expect']);
233
+ const contradicted = (described.contradictions ?? [])
234
+ .map((c) => c.kind)
235
+ .filter((kind) => !isAdvisory(kind) && !isAbsenceDerived(kind));
236
+ const disagreement = 0 === contradicted.length ? {} : { contradicted };
233
237
  if (!parsed.success) {
234
- expectations.push({ declared: false });
238
+ expectations.push({ declared: false, ...disagreement });
235
239
  stepResults.push(described);
236
240
  }
237
241
  else {
@@ -240,6 +244,7 @@ export const ACT_SEQUENCE_TOOL = {
240
244
  expectations.push({
241
245
  declared: true,
242
246
  held,
247
+ ...disagreement,
243
248
  ...(verdict.observed === undefined ? {} : { observed: verdict.observed }),
244
249
  ...(verdict.expected === undefined ? {} : { expected: verdict.expected }),
245
250
  });
@@ -287,7 +292,19 @@ export const ACT_SEQUENCE_TOOL = {
287
292
  // Same as captureAct: no `active()` gate, so a sequence driven with nothing open still lands
288
293
  // in the ambient tape. A stalled plan is still excluded — those steps never ran.
289
294
  if (stalledAt === undefined) {
290
- deps.recordings.capture(compileSequenceStep(args, { count: inputSteps.length, steps: stepResults }));
295
+ // A sub-step keeps its `expect` only if it held — same rule as act_and_wait, so a flow
296
+ // never replays an expectation that was never once observed to hold.
297
+ const proved = inputSteps.map((raw, i) => {
298
+ const step = asRecord(raw);
299
+ const { expect: _expect, ...rest } = step;
300
+ return true === expectations[i]?.held ? step : rest;
301
+ });
302
+ const recorded = compileSequenceStep({ ...args, steps: proved }, {
303
+ count: inputSteps.length,
304
+ steps: stepResults,
305
+ });
306
+ deps.recordings.capture(pageBefore === undefined ? recorded : { ...recorded, page: pageBefore });
307
+ deps.recordings.markEnded(pathOf(session.url));
291
308
  }
292
309
  /*
293
310
  * The grade, and the coverage it is never allowed to hide.
@@ -3,23 +3,8 @@ import { aliasParam } from './args/alias-args.js';
3
3
  import { resolveSessionWithin } from '../../portal/session/timing/resolve-within.js';
4
4
  import { WALL_CLOCK } from '../../portal/session/timing/wall-clock.js';
5
5
  import { timeoutMsSchema } from './args/numeric-bounds.js';
6
- import { captureAct } from '../../language/flows/replay.js';
7
- /**
8
- * The pathname of a session's live url, or undefined when it has none yet.
9
- *
10
- * Pathname rather than the whole url, because that is what `startPath` is compared against and what
11
- * a hash-routed app makes meaningless in the document location — see routeOfEvent.
12
- */
13
- function pathOf(url) {
14
- if (url === undefined)
15
- return undefined;
16
- try {
17
- return new URL(url).pathname;
18
- }
19
- catch {
20
- return undefined;
21
- }
22
- }
6
+ import { captureAct, captureVerdictedAct, pathOf } from '../../language/flows/replay.js';
7
+ import { durableArg, durableOutput, reloadAndRecheck, withDurability } from './act/durable.js';
23
8
  import { ActionType, ActionWarning, ReticleCommand, CaptureLoss, DEFAULT_ASSERT_TIMEOUT_MS, InputMode, Verified, VerifiedReason, PredicateKind, } from '@reticlehq/core';
24
9
  import { leanActResult, mutatedWithin } from './act/act-view.js';
25
10
  import { ReticleTool } from '@reticlehq/core';
@@ -37,8 +22,9 @@ import { intentDebt, openSessionIntents } from '../../memory/intent/open-intents
37
22
  import { dischargeInlineIntent, inlineVerdictId, linkInlineIntent, } from '../../memory/intent/inline-intent.js';
38
23
  import { declaresState } from '@reticlehq/engine/question/predicate/predicate-asks.js';
39
24
  import { isStateUnwatched } from '@reticlehq/engine/evidence/blind-spots.js';
40
- import { inFlightRequestLabels, repeatedRequestLabels, waitForInFlight, } from './act/settle-in-flight.js';
41
- import { waitForReaction } from './act/react-grace.js';
25
+ import { inFlightRequestLabels, repeatedRequestLabels } from './act/settle-in-flight.js';
26
+ import { finishAfterMatch } from './act/after-match.js';
27
+ import { noteProved } from './act/proved-controls.js';
42
28
  import { decideVerified } from '@reticlehq/engine/evidence/verified.js';
43
29
  import { honestyForVerdict } from '@reticlehq/engine/evidence/honesty.js';
44
30
  import { declaredExpectations, declaresBodyIndependentChannel, } from '@reticlehq/engine/question/declared.js';
@@ -76,8 +62,8 @@ import { readAlreadyTrue } from './act/already-true.js';
76
62
  /**
77
63
  * The action vocabulary, derived from ActionType — never retyped.
78
64
  *
79
- * The description used to list thirteen actions while ActionType had seventeen: blur, upload, drag
80
- * and webmcp were real, callable, and undocumented, because a hand-copied list drifts the moment
65
+ * The description used to omit callable actions such as blur, upload and drag,
66
+ * because a hand-copied list drifts the moment
81
67
  * someone adds an arm. Deriving both the schema and the prose from the enum makes that impossible.
82
68
  *
83
69
  * The handler already refused an unknown action with a good message; the schema now refuses it
@@ -254,6 +240,7 @@ export const ACT_TOOLS = [
254
240
  });
255
241
  }
256
242
  finally {
243
+ deps.recordings.markEnded(pathOf(session.url));
257
244
  // Close the window on every exit (settle or throw), recording the action + settle outcome.
258
245
  session.finishAction(undefined, settledOutcome, true === settledOutcome ? session.elapsed() - since : undefined);
259
246
  }
@@ -286,7 +273,7 @@ export const ACT_TOOLS = [
286
273
  .record(z.unknown())
287
274
  .optional()
288
275
  .describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut (Meta/Control/Shift/Alt), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { confirmDangerous: true } for a potentially destructive control. For upload: { path } is a path on disk (absolute or relative to project root; daemon reads real bytes) or { name, content?, type? } for inline bytes.'),
289
- predicate: PredicateSchema.optional().describe('Alias for `until` (the name reticle_assert / reticle_wait_for use).'),
276
+ predicate: PredicateSchema.optional().describe('Alias for `until`.'),
290
277
  until: PredicateSchema.optional().describe('Predicate to wait for after the action completes (same shape as reticle_assert). OMIT to wait for the page to SETTLE — network + DOM idle — the deterministic default instead of a sleep. To assert a consequence AND settle, allOf them: { kind: "allOf", predicates: [<your predicate>, { kind: "settled" }] }.'),
291
278
  timeout_ms: timeoutMsSchema
292
279
  .optional()
@@ -296,9 +283,11 @@ export const ACT_TOOLS = [
296
283
  .optional()
297
284
  .describe('Throw if the tab is throttled. Default: false.'),
298
285
  intent: intentArg,
286
+ durable: durableArg,
299
287
  ...sessionIdShape,
300
288
  },
301
289
  outputSchema: {
290
+ durable: durableOutput,
302
291
  effect: z
303
292
  .unknown()
304
293
  .describe('The reticle_act result (dispatched, settled, inputMode, etc.).'),
@@ -433,6 +422,9 @@ export const ACT_TOOLS = [
433
422
  const { alreadyTrue, alreadyTrueHiddenMatch, alreadyTrueEvidence, baselines } = await readAlreadyTrue(session, until, since);
434
423
  // Same as the ACT handler: the route this step RAN on, before the action can move the app.
435
424
  const routeBeforeWait = pathOf(session.url);
425
+ // Recorded in `finally`, once the verdict is known — see captureVerdictedAct.
426
+ let dispatched;
427
+ let recordedVerdict;
436
428
  try {
437
429
  // actCommand is the single interception point for upload+path rewrite.
438
430
  //
@@ -450,7 +442,7 @@ export const ACT_TOOLS = [
450
442
  if (actResult !== null) {
451
443
  if (!actResult.ok)
452
444
  throw new Error(actResult.error ?? 'act failed');
453
- captureAct(deps.recordings, args, actResult.result, routeBeforeWait);
445
+ dispatched = { result: actResult.result };
454
446
  // Dispatched — now this act owns the cursor and the effect. Marking its OWN measurement
455
447
  // also stops the spread below from inheriting an earlier reticle_act's action and
456
448
  // mutation count.
@@ -476,34 +468,12 @@ export const ACT_TOOLS = [
476
468
  since = 0;
477
469
  session = followed.session;
478
470
  verdict = followed.verdict;
479
- // The predicate resolves the INSTANT it holds, which on an optimistically-navigating app is
480
- // while the write is still in flight — so the verdict was taken over a window the app had not
481
- // finished, and came back `unknown / unsettled` asking the CALLER to re-check. A login that
482
- // genuinely succeeded returned at 492ms of an 8000ms budget with every corroborating channel
483
- // agreeing. Spend the rest of the budget the caller already granted.
484
- //
485
- // This decides nothing: it waits for the answer instead of guessing at it. A response that
486
- // arrives as a FAILURE still contradicts — and now arrives INSIDE the window, where the
487
- // detector can see it at all — while a request still open when the budget genuinely runs out
488
- // reports unsettled exactly as before, an honest limit rather than an early exit. Costs
489
- // nothing on the common path, where no request is in flight.
471
+ // After a match, let the app finish before the window closes — see act/after-match.ts.
490
472
  if (verdict.pass && timeout > 0) {
491
- const sleep = {
492
- sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
493
- };
494
- const spent = () => timeout - (session.elapsed() - predicateStarted);
495
- await waitForInFlight(session, since, spent(), sleep);
496
- // Waiting for the response moved the hazard rather than removing it: the response now lands
497
- // inside the window BY DESIGN, and the app's re-render happens a task or two later. Close
498
- // the window in that gap and every channel agrees the app took a successful write and did
499
- // nothing — `response-ignored`, i.e. `verified:"no"` on a correct app, produced entirely by
500
- // where we stopped looking. A false accusation is the more damaging direction of error for
501
- // a verification tool: it sends someone to fix code that is not broken.
502
- //
503
- // So the response is not the end of the window; the app's REACTION to it is. Paid only in
504
- // the shape that would otherwise be accused — a successful mutating write with nothing
505
- // moved after it — and short enough that a genuinely dropped response is still reported.
506
- await waitForReaction(session, since, spent(), sleep);
473
+ const left = () => timeout - (session.elapsed() - predicateStarted);
474
+ const reverted = await finishAfterMatch(session, until, since, baselines, left);
475
+ if (reverted !== undefined)
476
+ verdict = { pass: false, failureReason: reverted, observed: reverted };
507
477
  }
508
478
  const r = asRecord(actResult?.result);
509
479
  if ('boolean' === typeof r['settled'])
@@ -650,7 +620,7 @@ export const ACT_TOOLS = [
650
620
  const outcomePending = acceptedWriteLabels(windowEvents);
651
621
  const outcomeUnread = unreadWriteLabels(windowEvents);
652
622
  const stillInFlight = inFlightRequestLabels(windowEvents, session.url, session.background);
653
- const decision = decideVerified({
623
+ const { decision, durable } = await withDurability(args['durable'], until, decideVerified({
654
624
  pass: verdict.pass,
655
625
  // Threaded rather than looked up: decideVerified is pure and has no session.
656
626
  ...sessionVerdictFacts(session),
@@ -690,7 +660,9 @@ export const ACT_TOOLS = [
690
660
  repeated: repeatedRequestLabels(windowEvents),
691
661
  },
692
662
  ...(namedNetIsInFlight(until, stillInFlight) ? { namedRequestInFlight: true } : {}),
693
- });
663
+ }), (part) => reloadAndRecheck(deps, session, part, timeout));
664
+ recordedVerdict = String(decision.verified);
665
+ noteProved(session, recordedVerdict, actResult?.result);
694
666
  // Computed once: the verdict block reports it, and the instrumentation gaps are a second
695
667
  // reading of the same evidence rather than a new observation.
696
668
  const actionSummary = causalSummary(windowEvents, {
@@ -818,6 +790,7 @@ export const ACT_TOOLS = [
818
790
  // agent cannot miss it by not asking. Omitted entirely when clean, so a healthy action
819
791
  // pays nothing.
820
792
  ...(contradictions.length > 0 ? { contradictions } : {}),
793
+ ...(durable === undefined ? {} : { durable }),
821
794
  honesty: honestyForVerdict(String(decision.verified), honesty),
822
795
  // Without its summary: it is byte-identical to `summary` two fields up. See wireCapsule.
823
796
  ...(capsule === undefined ? {} : { capsule: wireCapsule(capsule) }),
@@ -828,6 +801,9 @@ export const ACT_TOOLS = [
828
801
  });
829
802
  }
830
803
  finally {
804
+ if (dispatched !== undefined)
805
+ captureVerdictedAct(deps.recordings, args, dispatched.result, routeBeforeWait, recordedVerdict);
806
+ deps.recordings.markEnded(pathOf(currentOf(deps.sessions, session).url));
831
807
  acted.finishAction(verdictEffect, settledOutcome, true === settledOutcome ? acted.elapsed() - actedSince : undefined);
832
808
  }
833
809
  },
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * Pure: no IO, no clock.
12
12
  */
13
- import { isConsequenceKind, isPresenceKind, PredicateKind } from '@reticlehq/core';
13
+ import { compareSourceClauses, isConsequenceKind, isPresenceKind, PredicateKind, } from '@reticlehq/core';
14
14
  import { HonestyGrade, strongerGrade, weakerGrade } from '@reticlehq/engine/evidence/honesty.js';
15
15
  export const PRESENCE_ONLY_ADVICE = 'This predicate only checks element/text presence, not an observable consequence. A locator healed to the wrong element (or a stale render) can satisfy it while the feature is broken. Prefer a { signal } or { net } assertion (or allOf it with one) so green means the feature actually worked.';
16
16
  function walk(predicate, negated = false) {
@@ -27,6 +27,14 @@ function walk(predicate, negated = false) {
27
27
  // never matched anything in the first place. Negation makes a presence check weaker, not stronger.
28
28
  if (PredicateKind.NOT === predicate.kind)
29
29
  return walk(predicate.predicate, true);
30
+ // A comparison grades as what its sides read: with a net/signal/state side it relates the page to
31
+ // the app's own record, which a wrong locator cannot fake; text against text is two readings of
32
+ // the same DOM. Its text side is always a content check, so it never counts as bare presence
33
+ // unless negated.
34
+ if (PredicateKind.COMPARE === predicate.kind) {
35
+ const consequence = compareSourceClauses(predicate).some((c) => isConsequenceKind(c.kind));
36
+ return { consequence, presence: !consequence && negated };
37
+ }
30
38
  // Leaf: classify against the single source of truth in core. Kinds that are neither consequence
31
39
  // nor presence (route/console/settled/animation) correctly return false for both.
32
40
  return {
@@ -109,6 +117,9 @@ export function gradeOfPredicate(predicate) {
109
117
  // verdict that proved only presence report `signal`, and a `minGrade: net` gate would trust it.
110
118
  case PredicateKind.ANY_OF:
111
119
  return combine(predicate.predicates, weakerGrade);
120
+ // Both sides had to be read and agree, so it proves as much as its stronger side.
121
+ case PredicateKind.COMPARE:
122
+ return combine(compareSourceClauses(predicate), strongerGrade);
112
123
  // Everything else, `not` included: an absence claim is satisfied trivially by a locator that
113
124
  // never matched anything, which is the argument `walk` already makes about negation.
114
125
  default:
@@ -1,4 +1,4 @@
1
- import { CaptureLoss, channelsRead, PredicateKind } from '@reticlehq/core';
1
+ import { CaptureLoss, channelsReadBy, PredicateKind } from '@reticlehq/core';
2
2
  import { sessionVerdictFacts } from '../../../portal/session/session-verdict-facts.js';
3
3
  import { gapsForAction } from '@reticlehq/engine/evidence/instrumentation-gaps.js';
4
4
  import { noteSessionGaps } from '@reticlehq/engine/evidence/gap-ledger.js';
@@ -148,7 +148,7 @@ changeUndeclared) {
148
148
  // first clause, and one this implementation could not run until the page started declaring.
149
149
  // Both halves are conditional on purpose: an SDK too old to declare sends nothing, and
150
150
  // treating that silence as an empty set would refuse every claim from every older page.
151
- channelsRead: channelsRead(predicate.kind),
151
+ channelsRead: channelsReadBy(predicate),
152
152
  ...(session.channels === undefined ? {} : { channelsObservable: session.channels }),
153
153
  // Threaded rather than looked up: decideVerified is pure and has no session.
154
154
  ...sessionVerdictFacts(session),
@@ -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
  },
@@ -29,6 +29,17 @@ function unknownParamsError(toolName, unknown) {
29
29
  const TOOLS_LIST_ACTION = 'list';
30
30
  /** What to write instead — the one parameter reticle_tools declares, and the no-argument form. */
31
31
  const TOOLS_ARG_HINT = 'name the tools you want in `names`: reticle_tools { names: ["reticle_act_and_wait"] } — or call it with no arguments for the full catalog';
32
+ /**
33
+ * The refusal for arguments of the wrong type, naming each offending key and what it declared.
34
+ * `path` is joined for a nested value (`intents.0.id`); a missing required key reads as the key.
35
+ */
36
+ function invalidParamsError(toolName, issues) {
37
+ const named = issues.map((issue) => {
38
+ const where = 0 === issue.path.length ? '(arguments)' : issue.path.join('.');
39
+ return `${where} (${issue.message})`;
40
+ });
41
+ return `invalid ${1 === named.length ? 'parameter' : 'parameters'} for ${toolName}: ${named.join('; ')} — NOT applied, so any result would be an answer to a different question`;
42
+ }
32
43
  /** The keys not declared by `shape`, in call order. */
33
44
  function unknownKeys(args, shape) {
34
45
  const declared = new Set(Object.keys(shape));
@@ -263,8 +274,24 @@ callable) {
263
274
  hint: 'fix the arguments and call reticle_run again',
264
275
  };
265
276
  }
277
+ // Keys are not enough: the TYPES are the other half of what a direct call's SDK validation
278
+ // refuses. Unchecked here, `reticle_viewport { width: "1440" }` from a client that stringifies
279
+ // numbers reached the handler, which read a non-number as missing and set the page to 64x64,
280
+ // and `reticle_intent { action: "declare", intents: "[...]" }` stored nothing and answered
281
+ // `{ intents: [] }` (#1118). The parsed value goes on, as it does on the direct path, so a
282
+ // schema's defaults and transforms apply the same way through either door.
283
+ const typed = z.object(target.inputSchema).safeParse(callArgs);
284
+ if (!typed.success) {
285
+ return {
286
+ error: invalidParamsError(name, typed.error.issues),
287
+ tool: name,
288
+ params: paramInfo(target.inputSchema),
289
+ ...(target.example === undefined ? {} : { example: target.example }),
290
+ hint: 'fix the arguments and call reticle_run again',
291
+ };
292
+ }
266
293
  try {
267
- return await runTool(target, deps, callArgs);
294
+ return await runTool(target, deps, typed.data);
268
295
  }
269
296
  catch (error) {
270
297
  // The SAME payload the direct tool path builds — recovery included. Answering every failure
@@ -272,7 +299,7 @@ callable) {
272
299
  // is reached through here: a stale ref, a paused session, a missing pairing token all came
273
300
  // back as the agent's arguments being wrong, which is advice that spends the retry.
274
301
  const message = error instanceof Error ? error.message : String(error);
275
- return { ...takeVersionSkewOnto(buildErrorPayload(message)), tool: name };
302
+ return { ...takeVersionSkewOnto(buildErrorPayload(message), deps.peerSkew), tool: name };
276
303
  }
277
304
  },
278
305
  };