@reticlehq/server 3.9.0 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/SKILL.md +13 -12
  2. package/dist/command/cli/adhoc-suite.js +2 -0
  3. package/dist/command/cli/adhoc-verdict.d.ts +2 -43
  4. package/dist/command/cli/adhoc-verdict.js +50 -187
  5. package/dist/command/cli/answers/mcp-connection-hint.d.ts +2 -0
  6. package/dist/command/cli/answers/mcp-connection-hint.js +6 -0
  7. package/dist/command/cli/cli-doctor.js +1 -1
  8. package/dist/command/cli/cli-flow-commands.js +2 -9
  9. package/dist/command/cli/cli-kill.js +1 -1
  10. package/dist/command/cli/cli-parse-compare.d.ts +22 -0
  11. package/dist/command/cli/cli-parse-compare.js +75 -0
  12. package/dist/command/cli/cli-parse-grade.d.ts +36 -0
  13. package/dist/command/cli/cli-parse-grade.js +75 -0
  14. package/dist/command/cli/cli-parse-grammar.d.ts +2 -0
  15. package/dist/command/cli/cli-parse-grammar.js +18 -20
  16. package/dist/command/cli/cli-parse-verify.d.ts +10 -0
  17. package/dist/command/cli/cli-parse-verify.js +63 -14
  18. package/dist/command/cli/cli-parse.d.ts +17 -15
  19. package/dist/command/cli/cli-parse.js +44 -27
  20. package/dist/command/cli/cli-update-commands.d.ts +1 -1
  21. package/dist/command/cli/cli-update-commands.js +29 -20
  22. package/dist/command/cli/cli-usage.js +58 -29
  23. package/dist/command/cli/cli-verify.d.ts +11 -2
  24. package/dist/command/cli/cli-verify.js +27 -8
  25. package/dist/command/cli/cloud-cli.js +2 -119
  26. package/dist/command/cli/daemon-link/daemon-client.d.ts +45 -0
  27. package/dist/command/cli/daemon-link/daemon-client.js +205 -0
  28. package/dist/command/cli/doctor/doctor-daemon-line.js +6 -3
  29. package/dist/command/cli/doctor-repairs.js +1 -1
  30. package/dist/command/cli/ensure-daemon.d.ts +26 -0
  31. package/dist/command/cli/ensure-daemon.js +57 -0
  32. package/dist/command/cli/finished-run/grade-command.d.ts +23 -0
  33. package/dist/command/cli/finished-run/grade-command.js +180 -0
  34. package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +1 -1
  35. package/dist/command/cli/lifecycle/daemon-lifecycle.js +1 -1
  36. package/dist/command/cli/ports/port-holder.d.ts +9 -0
  37. package/dist/command/cli/ports/port-holder.js +27 -3
  38. package/dist/command/cli/status/status-lines.d.ts +1 -0
  39. package/dist/command/cli/status/status-lines.js +22 -0
  40. package/dist/command/cli/status-fields.d.ts +5 -1
  41. package/dist/command/cli/status-fields.js +7 -3
  42. package/dist/command/cli/two-builds/compare-command.d.ts +48 -0
  43. package/dist/command/cli/two-builds/compare-command.js +314 -0
  44. package/dist/command/cli/verify-against.d.ts +61 -0
  45. package/dist/command/cli/verify-against.js +187 -0
  46. package/dist/command/cli/verify-no-session.d.ts +2 -0
  47. package/dist/command/cli/verify-no-session.js +13 -0
  48. package/dist/command/cli/verify-results.d.ts +35 -2
  49. package/dist/command/cli/verify-results.js +76 -10
  50. package/dist/command/cli.js +46 -41
  51. package/dist/command/daemon/daemon-resolve.d.ts +8 -1
  52. package/dist/command/daemon/daemon-resolve.js +27 -1
  53. package/dist/command/daemon/daemon.d.ts +12 -1
  54. package/dist/command/daemon/daemon.js +35 -4
  55. package/dist/command/daemon/lifetime/death-cause.js +21 -2
  56. package/dist/command/daemon/lifetime/heartbeat.js +7 -2
  57. package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
  58. package/dist/command/daemon/lifetime/startup-failure.js +62 -0
  59. package/dist/command/setup/bringup/dev-server-files.d.ts +9 -0
  60. package/dist/command/setup/bringup/dev-server-files.js +17 -0
  61. package/dist/command/setup/bringup/static-page-server.d.ts +14 -2
  62. package/dist/command/setup/bringup/static-page-server.js +43 -9
  63. package/dist/command/setup/init/init-runtime.js +6 -0
  64. package/dist/command/setup/node-effects.d.ts +3 -2
  65. package/dist/command/setup/node-effects.js +9 -3
  66. package/dist/command/setup/remaining-steps.d.ts +8 -0
  67. package/dist/command/setup/remaining-steps.js +10 -0
  68. package/dist/command/setup/run-setup.d.ts +2 -0
  69. package/dist/command/setup/run-setup.js +9 -1
  70. package/dist/command/setup/setup-command.js +6 -3
  71. package/dist/command/update/updater.js +6 -0
  72. package/dist/command/version/version-skew.js +1 -1
  73. package/dist/features/compare/action-plan.d.ts +260 -0
  74. package/dist/features/compare/action-plan.js +543 -0
  75. package/dist/features/compare/behaviour-compare.d.ts +24 -0
  76. package/dist/features/compare/behaviour-compare.js +85 -0
  77. package/dist/features/compare/behaviour-diff.d.ts +97 -0
  78. package/dist/features/compare/behaviour-diff.js +202 -0
  79. package/dist/features/compare/behaviour-report.d.ts +40 -0
  80. package/dist/features/compare/behaviour-report.js +128 -0
  81. package/dist/features/compare/build-readings.d.ts +106 -0
  82. package/dist/features/compare/build-readings.js +250 -0
  83. package/dist/features/compare/compare-diff.d.ts +62 -0
  84. package/dist/features/compare/compare-diff.js +177 -0
  85. package/dist/features/compare/compare-report-html.d.ts +6 -0
  86. package/dist/features/compare/compare-report-html.js +102 -0
  87. package/dist/features/compare/compare-report.d.ts +15 -0
  88. package/dist/features/compare/compare-report.js +73 -0
  89. package/dist/features/compare/compare-run.d.ts +60 -0
  90. package/dist/features/compare/compare-run.js +123 -0
  91. package/dist/features/compare/outside/effect-lines.d.ts +26 -0
  92. package/dist/features/compare/outside/effect-lines.js +103 -0
  93. package/dist/features/compare/outside/state-sources.d.ts +76 -0
  94. package/dist/features/compare/outside/state-sources.js +164 -0
  95. package/dist/features/compare/outside/value-dictionary.d.ts +51 -0
  96. package/dist/features/compare/outside/value-dictionary.js +227 -0
  97. package/dist/features/compare/visible-content.d.ts +20 -0
  98. package/dist/features/compare/visible-content.js +125 -0
  99. package/dist/features/crawl/crawl-empty.js +2 -1
  100. package/dist/features/crawl/crawl-exhaustive.d.ts +55 -0
  101. package/dist/features/crawl/crawl-exhaustive.js +138 -0
  102. package/dist/features/crawl/crawl-tools.d.ts +9 -2
  103. package/dist/features/crawl/crawl-tools.js +28 -12
  104. package/dist/features/crawl/crawl.d.ts +8 -9
  105. package/dist/features/crawl/crawl.js +61 -65
  106. package/dist/features/exhaust/app-coverage.d.ts +5 -1
  107. package/dist/features/exhaust/app-coverage.js +3 -1
  108. package/dist/features/exhaust/code-coverage.js +8 -4
  109. package/dist/features/exhaust/edge-cases.d.ts +133 -0
  110. package/dist/features/exhaust/edge-cases.js +310 -0
  111. package/dist/features/exhaust/edges/edge-drive.d.ts +42 -0
  112. package/dist/features/exhaust/edges/edge-drive.js +364 -0
  113. package/dist/features/exhaust/edges/edge-run.d.ts +59 -0
  114. package/dist/features/exhaust/edges/edge-run.js +169 -0
  115. package/dist/features/exhaust/explorer.d.ts +87 -14
  116. package/dist/features/exhaust/explorer.js +159 -25
  117. package/dist/features/exhaust/failure-branches.d.ts +5 -0
  118. package/dist/features/exhaust/failure-branches.js +29 -11
  119. package/dist/features/exhaust/invariants.d.ts +112 -0
  120. package/dist/features/exhaust/invariants.js +188 -0
  121. package/dist/features/exhaust/ledger.d.ts +68 -0
  122. package/dist/features/exhaust/ledger.js +121 -13
  123. package/dist/features/exhaust/session-fold.d.ts +10 -0
  124. package/dist/features/exhaust/session-fold.js +46 -3
  125. package/dist/features/exhaust/session-port.d.ts +3 -1
  126. package/dist/features/exhaust/session-port.js +97 -28
  127. package/dist/features/grade/grade-answer.d.ts +99 -0
  128. package/dist/features/grade/grade-answer.js +112 -0
  129. package/dist/features/grade/grade-noise.d.ts +27 -0
  130. package/dist/features/grade/grade-noise.js +55 -0
  131. package/dist/features/grade/grade-renumber.d.ts +12 -0
  132. package/dist/features/grade/grade-renumber.js +86 -0
  133. package/dist/features/grade/grade-state.d.ts +50 -0
  134. package/dist/features/grade/grade-state.js +93 -0
  135. package/dist/features/grade/grade-suggest.d.ts +33 -0
  136. package/dist/features/grade/grade-suggest.js +98 -0
  137. package/dist/features/visual/visual-tools.js +66 -19
  138. package/dist/index.js +7 -4
  139. package/dist/judgement/capsule/causal-summary.js +9 -2
  140. package/dist/judgement/runs/artifact/replay-mapping.d.ts +13 -1
  141. package/dist/judgement/runs/artifact/replay-mapping.js +1 -1
  142. package/dist/judgement/runs/reticle-runner.js +16 -2
  143. package/dist/judgement/runs/runner-port.d.ts +2 -0
  144. package/dist/judgement/runs/runner-port.js +23 -5
  145. package/dist/language/flows/affected-tools.d.ts +1 -1
  146. package/dist/language/flows/affected-tools.js +49 -30
  147. package/dist/language/flows/change/git-changed.d.ts +2 -1
  148. package/dist/language/flows/change/git-changed.js +19 -2
  149. package/dist/language/flows/fields/flow-secret-field.d.ts +1 -13
  150. package/dist/language/flows/fields/flow-secret-field.js +77 -21
  151. package/dist/language/flows/flow-anchor.d.ts +60 -2
  152. package/dist/language/flows/flow-anchor.js +125 -20
  153. package/dist/language/flows/flow-classify.d.ts +20 -0
  154. package/dist/language/flows/flow-classify.js +90 -12
  155. package/dist/language/flows/flow-cross-step.d.ts +7 -1
  156. package/dist/language/flows/flow-cross-step.js +25 -1
  157. package/dist/language/flows/flow-journey.d.ts +38 -4
  158. package/dist/language/flows/flow-journey.js +138 -8
  159. package/dist/language/flows/flow-replay-run.d.ts +0 -16
  160. package/dist/language/flows/flow-replay-run.js +76 -14
  161. package/dist/language/flows/flow-replay-types.d.ts +4 -2
  162. package/dist/language/flows/flow-replay.d.ts +19 -1
  163. package/dist/language/flows/flow-replay.js +169 -52
  164. package/dist/language/flows/flow-replayable.js +7 -1
  165. package/dist/language/flows/flow-start-path.d.ts +34 -5
  166. package/dist/language/flows/flow-start-path.js +202 -15
  167. package/dist/language/flows/flow-step-runners.d.ts +14 -13
  168. package/dist/language/flows/flow-step-runners.js +184 -109
  169. package/dist/language/flows/flow-store-for-session.d.ts +26 -25
  170. package/dist/language/flows/flow-store-for-session.js +44 -0
  171. package/dist/language/flows/flow-tools.js +46 -33
  172. package/dist/language/flows/flows.js +176 -76
  173. package/dist/language/flows/recording/tape/recordings.d.ts +19 -3
  174. package/dist/language/flows/recording/tape/recordings.js +71 -5
  175. package/dist/language/flows/replay.d.ts +19 -5
  176. package/dist/language/flows/replay.js +99 -7
  177. package/dist/language/flows/steps/page-steps.d.ts +68 -0
  178. package/dist/language/flows/steps/page-steps.js +198 -0
  179. package/dist/language/flows/suite/sequential-run.d.ts +68 -0
  180. package/dist/language/flows/suite/sequential-run.js +96 -0
  181. package/dist/language/flows/verify-change-tools.js +26 -15
  182. package/dist/memory/cloud/background-sync.d.ts +13 -0
  183. package/dist/memory/cloud/background-sync.js +24 -3
  184. package/dist/memory/cloud/sync-daemon.js +26 -27
  185. package/dist/memory/journal/attach-journal.js +2 -1
  186. package/dist/memory/journal/drive-flow.d.ts +7 -0
  187. package/dist/memory/journal/drive-flow.js +44 -13
  188. package/dist/memory/journal/journal-recorder.d.ts +4 -2
  189. package/dist/memory/journal/session-journal.d.ts +7 -0
  190. package/dist/memory/journal/session-journal.js +58 -10
  191. package/dist/memory/project/root-claims.d.ts +9 -0
  192. package/dist/memory/project/root-claims.js +9 -4
  193. package/dist/portal/input/download-capture.d.ts +7 -0
  194. package/dist/portal/input/download-capture.js +92 -0
  195. package/dist/portal/input/network-detail.d.ts +6 -0
  196. package/dist/portal/input/network-detail.js +12 -1
  197. package/dist/portal/input/network-mock-tools.js +49 -13
  198. package/dist/portal/input/network-mock.d.ts +8 -0
  199. package/dist/portal/input/network-mock.js +28 -9
  200. package/dist/portal/input/page-capture.d.ts +24 -0
  201. package/dist/portal/input/page-capture.js +33 -0
  202. package/dist/portal/input/real-input.d.ts +22 -20
  203. package/dist/portal/input/real-input.js +41 -31
  204. package/dist/portal/input/viewport-tools.d.ts +8 -0
  205. package/dist/portal/input/viewport-tools.js +75 -18
  206. package/dist/portal/pool/browser-pool.d.ts +11 -29
  207. package/dist/portal/pool/browser-pool.js +35 -17
  208. package/dist/portal/pool/context-permissions.d.ts +17 -3
  209. package/dist/portal/pool/context-permissions.js +36 -5
  210. package/dist/portal/pool/determinism.d.ts +52 -0
  211. package/dist/portal/pool/determinism.js +259 -0
  212. package/dist/portal/pool/lease-emulation.d.ts +34 -1
  213. package/dist/portal/pool/lease-emulation.js +18 -0
  214. package/dist/portal/pool/lease-marker-stamp.d.ts +6 -0
  215. package/dist/portal/pool/lease-marker-stamp.js +15 -1
  216. package/dist/portal/pool/lease-page-error.d.ts +44 -17
  217. package/dist/portal/pool/lease-page-error.js +147 -5
  218. package/dist/portal/pool/page-error-journal.d.ts +7 -1
  219. package/dist/portal/pool/page-error-journal.js +27 -0
  220. package/dist/portal/pool/playwright-launcher.js +51 -8
  221. package/dist/portal/pool/pool-contract.d.ts +27 -1
  222. package/dist/portal/pool/pool-lease.d.ts +36 -1
  223. package/dist/portal/pool/zero-install.d.ts +8 -0
  224. package/dist/portal/pool/zero-install.js +23 -1
  225. package/dist/portal/session/dev-server/dev-command.d.ts +7 -6
  226. package/dist/portal/session/dev-server/dev-command.js +61 -15
  227. package/dist/portal/session/dev-server/dev-server-probe.d.ts +7 -0
  228. package/dist/portal/session/dev-server/dev-server-probe.js +9 -0
  229. package/dist/portal/session/dev-server/project-ports.d.ts +21 -0
  230. package/dist/portal/session/dev-server/project-ports.js +62 -0
  231. package/dist/portal/session/facts/departed-network.d.ts +66 -0
  232. package/dist/portal/session/facts/departed-network.js +142 -0
  233. package/dist/portal/session/no-session-diagnosis.d.ts +9 -0
  234. package/dist/portal/session/no-session-diagnosis.js +67 -25
  235. package/dist/portal/session/no-session-next-action.d.ts +2 -0
  236. package/dist/portal/session/no-session-next-action.js +12 -3
  237. package/dist/portal/session/no-session-watch.d.ts +6 -0
  238. package/dist/portal/session/no-session-watch.js +38 -3
  239. package/dist/portal/session/serving-directory.d.ts +10 -0
  240. package/dist/portal/session/serving-directory.js +39 -8
  241. package/dist/portal/session/session-manager.d.ts +10 -0
  242. package/dist/portal/session/session-manager.js +174 -63
  243. package/dist/start-wiring.d.ts +2 -1
  244. package/dist/start-wiring.js +2 -1
  245. package/dist/surface/mcp/mcp.js +3 -2
  246. package/dist/surface/mcp/shared-params.d.ts +2 -0
  247. package/dist/surface/mcp/shared-params.js +31 -0
  248. package/dist/surface/tools/act/act-observation.d.ts +40 -2
  249. package/dist/surface/tools/act/act-observation.js +118 -12
  250. package/dist/surface/tools/act/act-preflight.js +1 -1
  251. package/dist/surface/tools/act/act-sequence-retry.d.ts +23 -2
  252. package/dist/surface/tools/act/act-sequence-retry.js +61 -5
  253. package/dist/surface/tools/act/after-match.d.ts +4 -1
  254. package/dist/surface/tools/act/after-match.js +8 -1
  255. package/dist/surface/tools/act/already-true.js +1 -1
  256. package/dist/surface/tools/act/navigation/reload-result.d.ts +12 -1
  257. package/dist/surface/tools/act/navigation/reload-result.js +25 -2
  258. package/dist/surface/tools/act/resolve-target.d.ts +15 -1
  259. package/dist/surface/tools/act/resolve-target.js +24 -1
  260. package/dist/surface/tools/act/sequence-grade.d.ts +12 -0
  261. package/dist/surface/tools/act/sequence-grade.js +37 -26
  262. package/dist/surface/tools/act/settle-in-flight.js +27 -15
  263. package/dist/surface/tools/act-command.d.ts +23 -0
  264. package/dist/surface/tools/act-command.js +50 -0
  265. package/dist/surface/tools/act-sequence-tool.js +78 -50
  266. package/dist/surface/tools/act-tools.d.ts +1 -13
  267. package/dist/surface/tools/act-tools.js +74 -86
  268. package/dist/surface/tools/agent/build-staleness.d.ts +1 -1
  269. package/dist/surface/tools/agent/build-staleness.js +1 -1
  270. package/dist/surface/tools/assert/assert-verdict.js +16 -37
  271. package/dist/surface/tools/browser-tools.js +12 -6
  272. package/dist/surface/tools/compare-tools.d.ts +11 -0
  273. package/dist/surface/tools/compare-tools.js +94 -0
  274. package/dist/surface/tools/coverage-tools.js +1 -1
  275. package/dist/surface/tools/dynamic-tools.js +9 -2
  276. package/dist/surface/tools/error-recovery.d.ts +12 -1
  277. package/dist/surface/tools/error-recovery.js +91 -21
  278. package/dist/surface/tools/explore-tools.js +1 -1
  279. package/dist/surface/tools/invoke-tool.js +3 -2
  280. package/dist/surface/tools/lease-coverage-tools.d.ts +49 -0
  281. package/dist/surface/tools/lease-coverage-tools.js +128 -0
  282. package/dist/surface/tools/lease-hint.d.ts +35 -1
  283. package/dist/surface/tools/lease-hint.js +42 -0
  284. package/dist/surface/tools/lease-readiness.d.ts +26 -0
  285. package/dist/surface/tools/lease-readiness.js +79 -0
  286. package/dist/surface/tools/lease-seed.d.ts +14 -1
  287. package/dist/surface/tools/lease-seed.js +35 -3
  288. package/dist/surface/tools/lease-tools.d.ts +3 -41
  289. package/dist/surface/tools/lease-tools.js +62 -110
  290. package/dist/surface/tools/lease-url-identity.d.ts +46 -0
  291. package/dist/surface/tools/lease-url-identity.js +97 -0
  292. package/dist/surface/tools/merge-tools.js +99 -2
  293. package/dist/surface/tools/merged-name-redirect.js +5 -1
  294. package/dist/surface/tools/observe-tools.js +15 -9
  295. package/dist/surface/tools/read/console-pre-attach.d.ts +19 -0
  296. package/dist/surface/tools/read/console-pre-attach.js +23 -0
  297. package/dist/surface/tools/read-tools.js +33 -30
  298. package/dist/surface/tools/real-input-attempt.d.ts +0 -8
  299. package/dist/surface/tools/real-input-attempt.js +143 -28
  300. package/dist/surface/tools/session-ports.d.ts +14 -0
  301. package/dist/surface/tools/session-ports.js +53 -0
  302. package/dist/surface/tools/tool-kit.d.ts +6 -0
  303. package/dist/surface/tools/tool-kit.js +2 -3
  304. package/dist/surface/tools/tool-surface.js +0 -1
  305. package/dist/surface/tools/tools.js +28 -5
  306. package/dist/surface/tools/type-spelling.d.ts +13 -0
  307. package/dist/surface/tools/type-spelling.js +36 -0
  308. package/dist/telemetry/agent-client.d.ts +6 -0
  309. package/dist/telemetry/agent-client.js +10 -0
  310. package/dist/telemetry/argument-shape.js +1 -0
  311. package/dist/telemetry/tool-refused.d.ts +6 -3
  312. package/dist/telemetry/tool-refused.js +8 -3
  313. package/docs/actions.mdx +3 -3
  314. package/docs/agent-cheatsheet.md +1 -1
  315. package/docs/capabilities.mdx +61 -1
  316. package/docs/cli/affected.mdx +9 -7
  317. package/docs/cli/cloud.mdx +3 -3
  318. package/docs/cli/compare.mdx +94 -0
  319. package/docs/cli/doctor.mdx +1 -1
  320. package/docs/cli/grade.mdx +97 -0
  321. package/docs/cli/license.mdx +9 -7
  322. package/docs/cli/restart.mdx +7 -5
  323. package/docs/cli/rollback.mdx +5 -5
  324. package/docs/cli/stop.mdx +3 -3
  325. package/docs/cli/update.mdx +2 -2
  326. package/docs/cli/verify.mdx +23 -1
  327. package/docs/cli.mdx +33 -23
  328. package/docs/deploy-checks.md +1 -1
  329. package/docs/desktop.mdx +1 -1
  330. package/docs/enterprise.md +2 -2
  331. package/docs/environment.mdx +1 -1
  332. package/docs/flows.md +11 -1
  333. package/docs/frameworks.mdx +9 -9
  334. package/docs/getting-started.md +1 -1
  335. package/docs/install-manual.mdx +1 -1
  336. package/docs/license-activation.mdx +3 -3
  337. package/docs/packages/tauri.mdx +1 -1
  338. package/docs/packages.mdx +1 -1
  339. package/docs/predicates.mdx +1 -1
  340. package/docs/quickstart.mdx +68 -49
  341. package/docs/skill-setup.md +1 -1
  342. package/docs/skill.md +14 -13
  343. package/docs/telemetry.md +2 -2
  344. package/docs/tools/navigate.mdx +1 -1
  345. package/docs/tools/verify.mdx +4 -1
  346. package/docs/troubleshooting.mdx +1 -1
  347. package/docs/usage.md +17 -14
  348. package/package.json +13 -13
  349. package/dist/features/exhaust/exhaust-run.d.ts +0 -33
  350. package/dist/features/exhaust/exhaust-run.js +0 -82
@@ -10,8 +10,15 @@
10
10
  * used from the two points where observation can be lost: a write rejected at dispatch, and a wait
11
11
  * cut off mid-predicate.
12
12
  */
13
- import { EventType, asRecord, asString, isDevToolingUrl } from '@reticlehq/core';
13
+ import { DriftReason, EventType, NetInitiator, asRecord, asString, isDevToolingUrl, } from '@reticlehq/core';
14
+ import { sdkBehindNote } from '../../../portal/session/session-info.js';
14
15
  import { awaitDocumentSuccessor, } from '../../../portal/session/session-successor.js';
16
+ /** How long an act whose page navigated away waits for the lease's successor to connect. */
17
+ export const NAVIGATION_FOLLOW_GRACE_MS = 8_000;
18
+ const REAL_CLOCK = {
19
+ now: () => Date.now(),
20
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
21
+ };
15
22
  /**
16
23
  * Follow the unique same-origin successor and re-ask there, or leave the verdict untouched.
17
24
  *
@@ -22,18 +29,65 @@ import { awaitDocumentSuccessor, } from '../../../portal/session/session-success
22
29
  */
23
30
  export async function followLostObservation(input) {
24
31
  const { sessions, session, verdict, timeout, predicateStarted, reevaluate } = input;
25
- if (true !== verdict.observationLost || 0 >= timeout)
32
+ const clock = input.clock ?? REAL_CLOCK;
33
+ const spent = session.elapsed() - predicateStarted;
34
+ const remaining = timeout - spent;
35
+ // Phase 1 — the wait was CUT OFF mid-predicate. The page re-announces on the new document;
36
+ // re-ask there with whatever budget is left.
37
+ if (true === verdict.observationLost && remaining > 0) {
38
+ const next = await awaitDocumentSuccessor(sessions, session, remaining);
39
+ if (null !== next) {
40
+ // Computed on the DEPARTED session, before the caller points at the successor: the new
41
+ // session's elapsed() starts at 0 and would silently refund the whole budget the caller had
42
+ // already spent.
43
+ const leftover = timeout - (session.elapsed() - predicateStarted);
44
+ if (leftover <= 0)
45
+ return { session: next, verdict, followed: true };
46
+ return { session: next, verdict: await reevaluate(next, leftover), followed: true };
47
+ }
48
+ }
49
+ // Phase 2 — gated on NAVIGATION EVIDENCE ALREADY IN THE WINDOW (#2009): a FAILURE graded while
50
+ // the action's own full-page navigation was still in flight reads "still on /login" for a
51
+ // sign-in that worked. The departure observer records the leaving as a NET_PENDING at dispatch
52
+ // time — while the old document's socket is still alive to deliver it — so the evidence is
53
+ // already in the buffer when the verdict is graded. Its presence unlocks the successor wait
54
+ // (bounded by the grace); its absence means the failure is about a document that never moved,
55
+ // and it stands UNCHANGED and AT ONCE — no wait, and the grace is never spent on it.
56
+ const grace = input.failedFollowGraceMs;
57
+ const registryCapable = 'function' === typeof sessions.get && 'function' === typeof sessions.all;
58
+ if (true === verdict.observationLost ||
59
+ true === verdict.pass ||
60
+ grace === undefined ||
61
+ input.since === undefined ||
62
+ !registryCapable) {
26
63
  return { session, verdict, followed: false };
27
- const remaining = timeout - (session.elapsed() - predicateStarted);
28
- const next = remaining > 0 ? await awaitDocumentSuccessor(sessions, session, remaining) : null;
29
- if (null === next)
64
+ }
65
+ const departurePending = session
66
+ .eventsSince(input.since)
67
+ .some((e) => e.type === EventType.NET_PENDING && e.data['initiator'] === NetInitiator.NAVIGATION);
68
+ if (!departurePending) {
30
69
  return { session, verdict, followed: false };
31
- // Computed on the DEPARTED session, before the caller points at the successor: the new session's
32
- // elapsed() starts at 0 and would silently refund the whole budget the caller had already spent.
33
- const leftover = timeout - (session.elapsed() - predicateStarted);
34
- if (leftover <= 0)
35
- return { session: next, verdict, followed: true };
36
- return { session: next, verdict: await reevaluate(next, leftover), followed: true };
70
+ }
71
+ const next = await awaitDocumentSuccessor(sessions, session, grace, clock);
72
+ if (null !== next) {
73
+ return {
74
+ session: next,
75
+ verdict: await reevaluate(next, grace),
76
+ followed: true,
77
+ budgetExtendedMs: grace,
78
+ };
79
+ }
80
+ return {
81
+ session,
82
+ followed: false,
83
+ verdict: {
84
+ ...verdict,
85
+ observationLost: true,
86
+ failureReason: `the act's page left its document and no successor session connected within ` +
87
+ `${String(grace)} ms. If this action navigated, the outcome after the navigation was ` +
88
+ 'never observed — re-run reticle_session { action: "list" } and re-drive on the live session.',
89
+ },
90
+ };
37
91
  }
38
92
  /** Why a consequence that did not follow a click on a disabled control is not the app's failure. */
39
93
  export const DISABLED_TARGET_NOTE = 'the control was disabled when it was acted on, so nothing a user could do there would have ' +
@@ -52,7 +106,9 @@ export function disabledTargetNote(pass, actResult) {
52
106
  const UNREACHABLE = {
53
107
  covered: (by) => `a person could not have clicked the target: it is covered by ${by}. Dismiss or move what ` +
54
108
  'covers it, then act again',
55
- NO_SIZE: 'a person could not have clicked the target: it has no size (0×0). Give it a size, or act on ' +
109
+ HIDDEN: 'a person could not have clicked the target: it is not visible (for example, it is inside a ' +
110
+ 'closed <details> or has visibility: hidden). Show it the way a person would, then act again',
111
+ NO_SIZE: 'a person could not have clicked the target: it has no size (zero width or zero height). Give it a size, or act on ' +
56
112
  'the control a person actually clicks',
57
113
  OFF_VIEWPORT: 'a person could not have clicked the target: it is outside the viewport even after scrolling ' +
58
114
  'it into view. Bring it on screen, then act again',
@@ -68,6 +124,11 @@ const UNREACHABLE = {
68
124
  export function unreachableTargetNote(actResult) {
69
125
  const effect = asRecord(asRecord(actResult)['effect']);
70
126
  if (true === effect['occluded']) {
127
+ // A hidden target keeps its box, so the hit test finds whatever is under it. That element
128
+ // covers nothing: the target is not shown.
129
+ if (false === effect['visible']) {
130
+ return UNREACHABLE.HIDDEN;
131
+ }
71
132
  const by = asString(effect['occluderName']) ?? asString(effect['occludedBy']);
72
133
  return UNREACHABLE.covered(by ?? 'another element');
73
134
  }
@@ -77,10 +138,55 @@ export function unreachableTargetNote(actResult) {
77
138
  return UNREACHABLE.OFF_VIEWPORT;
78
139
  return undefined;
79
140
  }
141
+ /**
142
+ * The drift for the first sub-step of an `ACT_SEQUENCE` result whose target no person could have
143
+ * clicked, or undefined. The page reports one effect for each sub-step it ran, in order. Synthetic
144
+ * dispatch reaches an unreachable target all the same, so a sequence is read sub-step by sub-step, as
145
+ * a single act is. `labelOf` names the sub-step, by its index in this result.
146
+ */
147
+ export function unreachableSubStepDrift(sequenceResult, labelOf) {
148
+ const effects = asRecord(sequenceResult)['effects'];
149
+ const list = Array.isArray(effects) ? effects : [];
150
+ for (let at = 0; at < list.length; at += 1) {
151
+ const reason = unreachableTargetNote({ effect: list[at] });
152
+ if (reason !== undefined) {
153
+ return {
154
+ reasonKind: DriftReason.TARGET_UNREACHABLE,
155
+ reason,
156
+ anchor: labelOf(at),
157
+ nearest: null,
158
+ };
159
+ }
160
+ }
161
+ return undefined;
162
+ }
80
163
  /** Why the acted target leaves the verdict undecided: no person could reach it, or it was disabled. */
81
164
  export function actTargetNote(pass, actResult) {
82
165
  return unreachableTargetNote(actResult) ?? disabledTargetNote(pass, actResult);
83
166
  }
167
+ // Unanchored: the engine prefixes the reason with "the page did not answer:".
168
+ const COMMAND_TIMEOUT = /command '[^']*' timed out after (\d+)ms/;
169
+ /**
170
+ * The cause and the one fix for a read that came back `unknown`, in place of a bare fact (#1990).
171
+ * A page whose SDK is behind this daemon may not serve `match` at all, so the fix is the same
172
+ * SDK-behind sentence the listing shows. On a current page the read ran out of the assertion's own
173
+ * budget, and the fix is more of it. Any reason that is not a command timeout is returned as it came.
174
+ */
175
+ export function diagnoseTimeout(reason, session) {
176
+ const granted = reason === undefined ? undefined : COMMAND_TIMEOUT.exec(reason)?.[1];
177
+ if (reason === undefined || granted === undefined) {
178
+ return reason;
179
+ }
180
+ const behind = sdkBehindNote(session);
181
+ if (behind !== undefined) {
182
+ return `${reason} ${behind}`;
183
+ }
184
+ return (`${reason} This read was bounded by what the assertion's budget had left ` +
185
+ `(${granted}ms): the budget ran out while the page was still working. ` +
186
+ 'Give the assertion more time with `timeout_ms`.');
187
+ }
188
+ /** Why an act's verdict is `unknown`: a read that timed out, else an unreachable or disabled target. */
189
+ export const actInconclusive = (verdict, actResult, session) => diagnoseTimeout(verdict.inconclusive, session) ?? actTargetNote(verdict.pass, actResult);
84
190
  /** Calls quoted on a failed check, and how much of each response body. */
85
191
  const MAX_NET_CALLS = 5;
86
192
  const MAX_BODY_CHARS = 300;
@@ -169,7 +169,7 @@ export function assertSequenceSteps(steps) {
169
169
  throw new Error(`step ${String(i)} has no \`ref\` or \`target\`. ` +
170
170
  'Sequence steps take `ref` (from reticle_query/reticle_snapshot) or `target` ' +
171
171
  '(e.g. { testid } or { label }), the same locator reticle_act accepts. ' +
172
- 'A press of Escape, Tab, or a modifier shortcut is a document key and needs neither. ' +
172
+ 'A press of Escape, Tab, an arrow key or a modifier shortcut, or any press with `document: true`, is a document key and needs neither. ' +
173
173
  'Nothing was acted on — the whole sequence is refused so a bad step cannot leave the earlier ones half-applied.');
174
174
  });
175
175
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * The two things a sequence step needs beyond a plain act: a retry when the ref goes stale under a
3
- * re-render, and a result shaped for the per-step report.
2
+ * The things a sequence step needs beyond a plain act: a retry when the ref goes stale under a
3
+ * re-render, a result shaped for the per-step report, and the `burst` pace (at the end).
4
4
  *
5
5
  * ── THE RETRY ─────────────────────────────────────────────────────────────────────────────────
6
6
  *
@@ -42,4 +42,25 @@ export declare function runStepWithStaleRetry(attempt: () => Promise<StepOutcome
42
42
  * nothing to look for".
43
43
  */
44
44
  export declare function describeStepResult(step: Record<string, unknown>, result: Record<string, unknown>): Record<string, unknown>;
45
+ /**
46
+ * Ceiling for a burst gap. Past this it is not a burst, it is a paced walk.
47
+ *
48
+ * HOW TO PICK A GAP, because both edges are real and neither is guessable. It must outlast the
49
+ * handler committing and dispatching its request, and undercut the response it is racing.
50
+ * Measured against a superseded-filter race (the superseded query 700ms, the one replacing it 90ms):
51
+ * at 0ms the first request was never issued at all, and at the ordinary settle (~500ms in a
52
+ * throttled tab, where rAF never fires and the frame budget always times out) the two clicks
53
+ * serialised and never overlapped. 50ms sat in the window. A different app needs a different
54
+ * number: read its own slow endpoint.
55
+ */
56
+ export declare const BURST_MAX_GAP_MS = 500;
57
+ /** A recorded gap as the live tool would apply it: clamped, or absent. */
58
+ export declare function burstGap(burstMs: number | undefined): number | undefined;
59
+ /** A burst step returns as soon as it is dispatched: the gap, not the settle, paces the sequence. */
60
+ export declare function pacedArgs(args: Record<string, unknown>, gap: number | undefined): Record<string, unknown>;
61
+ /**
62
+ * Wait out the rest of the gap that began when the previous step was dispatched, if there is one. The gap is between
63
+ * ACTIONS, as live, so time the previous step already took counts towards it.
64
+ */
65
+ export declare function holdGap(elapsed: () => number, since: number | undefined, gap: number | undefined, sleep: (ms: number) => Promise<void>): Promise<void>;
45
66
  export {};
@@ -1,6 +1,6 @@
1
1
  /**
2
- * The two things a sequence step needs beyond a plain act: a retry when the ref goes stale under a
3
- * re-render, and a result shaped for the per-step report.
2
+ * The things a sequence step needs beyond a plain act: a retry when the ref goes stale under a
3
+ * re-render, a result shaped for the per-step report, and the `burst` pace (at the end).
4
4
  *
5
5
  * ── THE RETRY ─────────────────────────────────────────────────────────────────────────────────
6
6
  *
@@ -30,8 +30,9 @@ const realSleep = {
30
30
  */
31
31
  export async function runStepWithStaleRetry(attempt, session, since, budgetMs, opts = realSleep) {
32
32
  const first = await attempt();
33
- if (first.ok || !isStaleRefError(first.error))
33
+ if (first.ok || !isStaleRefError(first.error)) {
34
34
  return first;
35
+ }
35
36
  await waitForReaction(session, since, budgetMs, opts);
36
37
  return attempt();
37
38
  }
@@ -51,9 +52,64 @@ export function describeStepResult(step, result) {
51
52
  settleReason: result['settleReason'] ?? null,
52
53
  };
53
54
  // `reticleUi`: the step hit Reticle's own HUD, so the recorder leaves it out of the app's flow.
54
- for (const key of ['testid', 'component', 'role', 'name', 'source', 'warning', 'reticleUi']) {
55
- if (result[key] !== undefined)
55
+ // `secret`: the SDK says the target holds a secret. Without it a sequence saved a password in clear.
56
+ // `nth`/`of`/`testidOf`: the control's position among namesakes, which the recorder anchors on.
57
+ // `componentNth`/`componentOf`: its position among the elements of its component anchor.
58
+ for (const key of [
59
+ 'testid',
60
+ 'testidOf',
61
+ 'testidNth',
62
+ 'nth',
63
+ 'of',
64
+ 'component',
65
+ 'componentNth',
66
+ 'componentOf',
67
+ 'ownText',
68
+ 'role',
69
+ 'name',
70
+ 'within',
71
+ 'source',
72
+ 'warning',
73
+ 'reticleUi',
74
+ 'secret',
75
+ ]) {
76
+ if (result[key] !== undefined) {
56
77
  out[key] = result[key];
78
+ }
57
79
  }
58
80
  return out;
59
81
  }
82
+ // ── THE BURST PACE ── shared by the live tool and the replay of a saved sequence (#2130). ──────
83
+ /**
84
+ * Ceiling for a burst gap. Past this it is not a burst, it is a paced walk.
85
+ *
86
+ * HOW TO PICK A GAP, because both edges are real and neither is guessable. It must outlast the
87
+ * handler committing and dispatching its request, and undercut the response it is racing.
88
+ * Measured against a superseded-filter race (the superseded query 700ms, the one replacing it 90ms):
89
+ * at 0ms the first request was never issued at all, and at the ordinary settle (~500ms in a
90
+ * throttled tab, where rAF never fires and the frame budget always times out) the two clicks
91
+ * serialised and never overlapped. 50ms sat in the window. A different app needs a different
92
+ * number: read its own slow endpoint.
93
+ */
94
+ export const BURST_MAX_GAP_MS = 500;
95
+ /** A recorded gap as the live tool would apply it: clamped, or absent. */
96
+ export function burstGap(burstMs) {
97
+ return burstMs === undefined ? undefined : Math.max(0, Math.min(burstMs, BURST_MAX_GAP_MS));
98
+ }
99
+ /** A burst step returns as soon as it is dispatched: the gap, not the settle, paces the sequence. */
100
+ export function pacedArgs(args, gap) {
101
+ return gap === undefined ? args : { ...args, settleMs: 0 };
102
+ }
103
+ /**
104
+ * Wait out the rest of the gap that began when the previous step was dispatched, if there is one. The gap is between
105
+ * ACTIONS, as live, so time the previous step already took counts towards it.
106
+ */
107
+ export async function holdGap(elapsed, since, gap, sleep) {
108
+ if (since === undefined || gap === undefined) {
109
+ return;
110
+ }
111
+ const remaining = gap - (elapsed() - since);
112
+ if (remaining > 0) {
113
+ await sleep(remaining);
114
+ }
115
+ }
@@ -9,9 +9,12 @@
9
9
  * nothing on its own.
10
10
  */
11
11
  import type { Predicate } from '@reticlehq/core';
12
- import { evaluatePredicate, type EvalResult } from '@reticlehq/engine/question/predicate/predicate.js';
12
+ import { evaluatePredicate, waitForPredicate, type EvalResult } from '@reticlehq/engine/question/predicate/predicate.js';
13
13
  import { waitForInFlight } from './settle-in-flight.js';
14
14
  import { revertedAfterMatch } from './durable.js';
15
+ type WaitArgs = Parameters<typeof waitForPredicate>;
16
+ /** Wait for the predicate inside the budget, or read it once when there is no budget. */
17
+ export declare function evaluateAfterAct(session: WaitArgs[0], until: Predicate, timeout: number, since: number, baselines: WaitArgs[4]): Promise<EvalResult>;
15
18
  type Waited = Parameters<typeof waitForInFlight>[0] & Parameters<typeof revertedAfterMatch>[0];
16
19
  /** Returns the sentence to fail with when the consequence reverted, else undefined. */
17
20
  export declare function finishAfterMatch(session: Waited, until: Predicate, since: number, baselines: Parameters<typeof revertedAfterMatch>[3], remaining: () => number): Promise<string | undefined>;
@@ -9,10 +9,17 @@
9
9
  * nothing on its own.
10
10
  */
11
11
  import { hiddenMatchNote } from '@reticlehq/engine/evidence/already-true.js';
12
- import { evaluatePredicate, } from '@reticlehq/engine/question/predicate/predicate.js';
12
+ import { evaluatePredicate, waitForPredicate, } from '@reticlehq/engine/question/predicate/predicate.js';
13
13
  import { waitForInFlight } from './settle-in-flight.js';
14
14
  import { waitForReaction } from './react-grace.js';
15
15
  import { revertedAfterMatch } from './durable.js';
16
+ /** Wait for the predicate inside the budget, or read it once when there is no budget. */
17
+ export function evaluateAfterAct(session, until, timeout, since, baselines) {
18
+ if (timeout > 0) {
19
+ return waitForPredicate(session, until, timeout, since, baselines);
20
+ }
21
+ return evaluatePredicate(session, until, since, true, baselines);
22
+ }
16
23
  /** Returns the sentence to fail with when the consequence reverted, else undefined. */
17
24
  export async function finishAfterMatch(session, until, since, baselines, remaining) {
18
25
  const sleep = { sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)) };
@@ -18,7 +18,7 @@ export async function readAlreadyTrue(session, until, since) {
18
18
  // baseline yet would report `inconclusive` — reading the app twice to answer a question we had
19
19
  // not taken the reading for.
20
20
  const baselines = await captureBaselines(session, until);
21
- const precheck = until !== undefined && readsDomState(until)
21
+ const precheck = until !== undefined && readsDomState(until, since)
22
22
  ? await evaluatePredicate(session, until, since, false, baselines)
23
23
  : undefined;
24
24
  const alreadyTrue = precheck?.pass ?? false;
@@ -16,4 +16,15 @@
16
16
  * branch reports it (navigate-result.ts): so "Reticle stopped waiting" is distinguishable from "the
17
17
  * page never came back", and the fix — a larger `timeout_ms` — is visible in the result.
18
18
  */
19
- export declare function reloadResult(reconnected: boolean, waitedMs: number): Record<string, unknown>;
19
+ export declare function reloadResult(reconnected: boolean, waitedMs: number, hard?: HardReload): Record<string, unknown>;
20
+ /**
21
+ * How a `hard` reload reached the page (#1785). A leased tab reloads through the browser with the
22
+ * cache ignored. Any other tab can only fetch its document again, so its modules can be stale.
23
+ */
24
+ export declare const HardReload: {
25
+ readonly CACHE_IGNORED: "cache_ignored";
26
+ readonly DOCUMENT_ONLY: "document_only";
27
+ };
28
+ export type HardReload = (typeof HardReload)[keyof typeof HardReload];
29
+ /** The `hardReload` field for a reload, or nothing when the call did not ask for `hard`. */
30
+ export declare function hardReloadOf(hard: boolean, throughBrowser: boolean): HardReload | undefined;
@@ -27,8 +27,31 @@ const RELOAD_RECONNECTED_NOTE = 'the page came back and re-announced itself unde
27
27
  * branch reports it (navigate-result.ts): so "Reticle stopped waiting" is distinguishable from "the
28
28
  * page never came back", and the fix — a larger `timeout_ms` — is visible in the result.
29
29
  */
30
- export function reloadResult(reconnected, waitedMs) {
31
- return reconnected
30
+ export function reloadResult(reconnected, waitedMs, hard) {
31
+ const base = reconnected
32
32
  ? { ok: true, confirmed: true, note: RELOAD_RECONNECTED_NOTE }
33
33
  : { ok: true, confirmed: false, waitedMs, note: RELOAD_NOTE };
34
+ if (hard === undefined) {
35
+ return base;
36
+ }
37
+ return { ...base, hardReload: hard, note: `${base.note} ${HARD_RELOAD_NOTE[hard]}` };
38
+ }
39
+ /**
40
+ * How a `hard` reload reached the page (#1785). A leased tab reloads through the browser with the
41
+ * cache ignored. Any other tab can only fetch its document again, so its modules can be stale.
42
+ */
43
+ export const HardReload = {
44
+ CACHE_IGNORED: 'cache_ignored',
45
+ DOCUMENT_ONLY: 'document_only',
46
+ };
47
+ const HARD_RELOAD_NOTE = {
48
+ [HardReload.CACHE_IGNORED]: 'The browser reloaded with its cache ignored, so every script and module was fetched again.',
49
+ [HardReload.DOCUMENT_ONLY]: 'This tab is not a lease, so only the document was fetched again. Scripts and modules can still come from the browser cache. To fetch them again, reload the tab in the browser with Cmd+Shift+R, or take a lease with reticle_lease.',
50
+ };
51
+ /** The `hardReload` field for a reload, or nothing when the call did not ask for `hard`. */
52
+ export function hardReloadOf(hard, throughBrowser) {
53
+ if (!hard) {
54
+ return undefined;
55
+ }
56
+ return throughBrowser ? HardReload.CACHE_IGNORED : HardReload.DOCUMENT_ONLY;
34
57
  }
@@ -2,6 +2,8 @@
2
2
  export type TargetResolution = {
3
3
  readonly kind: 'ref';
4
4
  readonly ref: string;
5
+ /** Set when every match was hidden and the action uses the hidden one. Says so to the caller. */
6
+ readonly hiddenTarget?: string;
5
7
  } | {
6
8
  readonly kind: 'global';
7
9
  readonly ref: '';
@@ -9,6 +11,16 @@ export type TargetResolution = {
9
11
  readonly kind: 'error';
10
12
  readonly message: string;
11
13
  };
14
+ /**
15
+ * What a result says when every match was hidden and the action used the hidden one. It does not
16
+ * say that a person could not reach it: a hidden input with a visible label is reached through the
17
+ * label, and the reach check after the action decides that.
18
+ */
19
+ export declare function hiddenTargetNote(ref: string): string;
20
+ /** The `hiddenTarget` field for a tool result, or nothing when the action used a visible match. */
21
+ export declare function hiddenTargetField(resolution: TargetResolution): {
22
+ hiddenTarget?: string;
23
+ };
12
24
  /** The words before the nearest-target list in a no_match refusal. The repeat hint reads after it. */
13
25
  export declare const NEAREST_TARGETS_LEAD = " The nearest targets on the page, most similar first: ";
14
26
  /**
@@ -26,7 +38,9 @@ export declare const NEAREST_TARGETS_LEAD = " The nearest targets on the page, m
26
38
  *
27
39
  * Invisible matches are filtered before counting: a hidden duplicate is a common way for a
28
40
  * legitimately unambiguous query to look ambiguous, and refusing on it would push callers back to
29
- * the two-turn path for no gain.
41
+ * the two-turn path for no gain. When every match is hidden, the one hidden match is used and the
42
+ * resolution names it in `hiddenTarget`. It is not refused: a hidden input is often clicked through
43
+ * its visible label.
30
44
  *
31
45
  * When several remain, they are ranked (in-viewport, then role) inside the refusal only — so the
32
46
  * agent can pass the top ref on the next turn without a snapshot. Ranking is never acted on here.
@@ -83,6 +83,23 @@ function nearNamesClause(hint) {
83
83
  'Use that name, or — when the name carries a part that changes, like a time — target it by ' +
84
84
  '`testid` or by `text` (a contained match).');
85
85
  }
86
+ /**
87
+ * What a result says when every match was hidden and the action used the hidden one. It does not
88
+ * say that a person could not reach it: a hidden input with a visible label is reached through the
89
+ * label, and the reach check after the action decides that.
90
+ */
91
+ export function hiddenTargetNote(ref) {
92
+ return (`every element that matched the target is hidden, so the action used the hidden match ${ref}. ` +
93
+ 'If a person reaches it through a visible label, this is correct. If not, target the control ' +
94
+ 'a person can see.');
95
+ }
96
+ /** The `hiddenTarget` field for a tool result, or nothing when the action used a visible match. */
97
+ export function hiddenTargetField(resolution) {
98
+ if ('ref' !== resolution.kind || resolution.hiddenTarget === undefined) {
99
+ return {};
100
+ }
101
+ return { hiddenTarget: resolution.hiddenTarget };
102
+ }
86
103
  const NearestTargetsSchema = z.array(NearestTargetSchema).min(1);
87
104
  /** The words before the nearest-target list in a no_match refusal. The repeat hint reads after it. */
88
105
  export const NEAREST_TARGETS_LEAD = ' The nearest targets on the page, most similar first: ';
@@ -117,7 +134,9 @@ function nearestClause(hint) {
117
134
  *
118
135
  * Invisible matches are filtered before counting: a hidden duplicate is a common way for a
119
136
  * legitimately unambiguous query to look ambiguous, and refusing on it would push callers back to
120
- * the two-turn path for no gain.
137
+ * the two-turn path for no gain. When every match is hidden, the one hidden match is used and the
138
+ * resolution names it in `hiddenTarget`. It is not refused: a hidden input is often clicked through
139
+ * its visible label.
121
140
  *
122
141
  * When several remain, they are ranked (in-viewport, then role) inside the refusal only — so the
123
142
  * agent can pass the top ref on the next turn without a snapshot. Ranking is never acted on here.
@@ -153,5 +172,9 @@ export function resolveTargetRef(candidates, hint) {
153
172
  if ('string' !== typeof ref || 0 === ref.length) {
154
173
  return { kind: 'error', message: 'target matched an element with no usable ref.' };
155
174
  }
175
+ // Every match was hidden, so the action uses a hidden one. Rule 11: say so, never silently.
176
+ if (0 === visible.length) {
177
+ return { kind: 'ref', ref, hiddenTarget: hiddenTargetNote(ref) };
178
+ }
156
179
  return { kind: 'ref', ref };
157
180
  }
@@ -46,6 +46,16 @@ export interface StepExpectation {
46
46
  * step is undecided, never a miss — `unknown`, as act_and_wait says.
47
47
  */
48
48
  undecided?: string;
49
+ /** The step's action (`click`, `fill`), so a plan that could not judge a step can name it. */
50
+ action?: string;
51
+ /** A real frame flushed after this step's action, as `settled` on its act result says. */
52
+ settled?: boolean;
53
+ /**
54
+ * Absence-derived contradiction kinds seen in this step's window (a request that never settled,
55
+ * for example). They do not decide a miss, but they stop a plan from claiming no channel saw a
56
+ * problem.
57
+ */
58
+ doubted?: readonly string[];
49
59
  }
50
60
  export interface SequenceGrade {
51
61
  verified: Verified;
@@ -69,6 +79,8 @@ interface SequencePlan {
69
79
  readonly planned: number;
70
80
  /** Whether anything actually reached the page. */
71
81
  readonly dispatched: boolean;
82
+ /** The capture lost events inside the plan's window, so no channel saw all of it. */
83
+ readonly captureLost?: boolean;
72
84
  }
73
85
  export declare function gradeSequence(steps: readonly StepExpectation[], plan?: SequencePlan): SequenceGrade;
74
86
  /**
@@ -23,6 +23,23 @@ import { Verified, VerifiedReason } from '@reticlehq/core';
23
23
  * sweep of independent controls, where stopping at the first would hide the other thirteen.
24
24
  */
25
25
  export const DeviationMode = { HALT: 'halt', CONTINUE: 'continue' };
26
+ /** The sentence for a plan whose steps declared nothing. */
27
+ function undeclaredBecause(total, dispatched) {
28
+ if (0 === total) {
29
+ return 'the plan had no steps, so nothing was driven and nothing was proved';
30
+ }
31
+ // A step refused before dispatch is a malformed call: telling its author to add an `expect`
32
+ // sends them to the wrong thing.
33
+ if (!dispatched) {
34
+ return `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\``;
35
+ }
36
+ return `all ${String(total)} step(s) declared nothing, so the app was driven but not verified — give each step an \`expect\` naming the consequence it causes`;
37
+ }
38
+ /** A step as a sentence names it: its number, counted from 1, and its action when it has one. */
39
+ function stepName(index, action) {
40
+ const number = `step ${String(index + 1)}`;
41
+ return action === undefined ? number : `${number} (${action})`;
42
+ }
26
43
  export function gradeSequence(steps, plan = { planned: steps.length, dispatched: true }) {
27
44
  const total = Math.max(plan.planned, steps.length);
28
45
  const declaring = steps.filter((step) => step.declared);
@@ -54,44 +71,37 @@ export function gradeSequence(steps, plan = { planned: steps.length, dispatched:
54
71
  because: `step ${String(contradictedAt + 1)}: channels disagree about this action (${kinds}) even though it went on`,
55
72
  };
56
73
  }
57
- // Nothing failed that was seen: a step nobody could judge leaves the plan undecided, saying why.
58
- const unjudged = steps.find((step) => step.undecided !== undefined);
59
- if (unjudged !== undefined)
74
+ // Nothing failed that was seen: a step nobody could judge leaves the plan undecided. The sentence
75
+ // names the step and what was missing, so the agent knows which step to change (#2204).
76
+ const unjudgedAt = steps.findIndex((step) => step.undecided !== undefined);
77
+ if (unjudgedAt >= 0) {
78
+ const unjudged = steps[unjudgedAt];
60
79
  return {
61
80
  verified: Verified.UNKNOWN,
62
81
  reason: VerifiedReason.INCONCLUSIVE,
63
82
  declared,
64
83
  total,
65
- because: `a step could not be judged — ${unjudged.undecided ?? ''}`,
84
+ because: `${stepName(unjudgedAt, unjudged?.action)} could not be judged: ${unjudged?.undecided ?? ''}`,
66
85
  };
86
+ }
67
87
  /*
68
- * `unknown` rather than `no-fault`, and the distinction is load-bearing.
69
- *
70
- * "Nothing was declared" is what `no-fault` means for a single action -- but the engine only says
71
- * it over a SETTLED window, because no-fault claims the whole window was observed. Nothing on
72
- * this path observes settledness: the caller tracks dispatch and stalls, never settle. Saying
73
- * `no-fault` here would claim evidence nobody collected, in the one place this product cannot
74
- * afford to overclaim.
75
- *
76
- * So the enum stays conservative and the SENTENCE carries the whole answer: it names declaring a
77
- * consequence as the next move, which is what an agent reading `no-fault` would have done.
78
- * Upgrading means plumbing `settled` from the act results through `SequencePlan`.
88
+ * Nothing declared is `no-fault` over a quiet plan and `unknown` otherwise. This is the rule a
89
+ * single action follows (`engine/src/evidence/verified.ts`): no-fault claims the whole window was
90
+ * observed, so every step must report a settled frame, no step may carry an absence-derived
91
+ * finding, and the capture must be whole. `unknown` sends an agent to look again with better
92
+ * coverage. When the plan was quiet, that cannot help: the fix is to declare a consequence.
79
93
  */
80
94
  if (0 === declared) {
95
+ const quiet = plan.dispatched &&
96
+ true !== plan.captureLost &&
97
+ steps.length > 0 &&
98
+ steps.every((step) => true === step.settled && 0 === (step.doubted ?? []).length);
81
99
  return {
82
- verified: Verified.UNKNOWN,
100
+ verified: quiet ? Verified.NO_FAULT : Verified.UNKNOWN,
83
101
  reason: VerifiedReason.NOTHING_DECLARED,
84
102
  declared,
85
103
  total,
86
- because: 0 === total
87
- ? 'the plan had no steps, so nothing was driven and nothing was proved'
88
- : // "the app was driven" was said unconditionally, including on a call whose own
89
- // `dispatched` was false in the same object. A step refused before dispatch is a
90
- // malformed call, and telling its author to add an `expect` sends them at the wrong
91
- // thing entirely.
92
- plan.dispatched
93
- ? `all ${String(total)} step(s) declared nothing, so the app was driven but not verified — give each step an \`expect\` naming the consequence it causes`
94
- : `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\``,
104
+ because: undeclaredBecause(total, plan.dispatched),
95
105
  };
96
106
  }
97
107
  // A journey is proved at its END. Declaring only early steps checks that it started, and a yes
@@ -144,8 +154,9 @@ export function gradeSequence(steps, plan = { planned: steps.length, dispatched:
144
154
  /** The stopped recording a proved journey is held under, so the `keep` offer can be taken. */
145
155
  export const JOURNEY_RECORDING = 'last-journey';
146
156
  export function offerToKeep(grade) {
147
- if (grade.verified !== Verified.YES || 0 === grade.declared)
157
+ if (grade.verified !== Verified.YES || 0 === grade.declared) {
148
158
  return undefined;
159
+ }
149
160
  return (`this plan proved something (${String(grade.declared)} of ${String(grade.total)} steps declared a ` +
150
161
  'consequence and every one held) — keep it as a regression flow with ' +
151
162
  `reticle_flow_save { flowName: "${JOURNEY_RECORDING}", saveAs: "<name>" }, and the steps that declared nothing will replay ` +