@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
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { checkWords, runIdOfCall } from '../../judgement/runs/drive-run.js';
6
6
  import { noteEmptyRead } from './gaps/observed-nothing.js';
7
+ import { withPreAttach } from './read/console-pre-attach.js';
7
8
  import { z } from 'zod';
8
9
  import { aliasParam } from './args/alias-args.js';
9
10
  import { CONSOLE_ATTACH_NOTE, CONSOLE_LEVELS, ReticleCommand, DEFAULT_ASSERT_TIMEOUT_MS, PredicateKind, Verified, checkIntent, } from '@reticlehq/core';
@@ -450,13 +451,15 @@ export const OBSERVE_TOOLS = [
450
451
  // Two nudges, both only on a GREEN verdict (a failing one is moot): a presence-only assertion
451
452
  // a wrong element can fake, and an assertion pinned to a status Reticle derived rather than one
452
453
  // a server sent. Neither blocks — the assertion still passes; they steer the next one.
453
- const advice = !verdict.pass
454
- ? {}
455
- : isPresenceOnlyAssertion(predicate)
456
- ? { advice: PRESENCE_ONLY_ADVICE }
457
- : assertsDerivedIpcStatus(predicate)
458
- ? { advice: DERIVED_IPC_STATUS_ADVICE }
459
- : {};
454
+ let advice = {};
455
+ if (verdict.pass) {
456
+ if (isPresenceOnlyAssertion(predicate)) {
457
+ advice = { advice: PRESENCE_ONLY_ADVICE };
458
+ }
459
+ else if (assertsDerivedIpcStatus(predicate)) {
460
+ advice = { advice: DERIVED_IPC_STATUS_ADVICE };
461
+ }
462
+ }
460
463
  // Asked of every verdict drawn after an observed edit, not once per edit — see
461
464
  // isChangeUndeclared for why repeating it is disclosure rather than nagging.
462
465
  // Read once: the undeclared-change check and the engine's claim reading share it.
@@ -609,7 +612,10 @@ export const OBSERVE_TOOLS = [
609
612
  * `true` and `false` keep their exact old meanings, so a caller that already passes one is
610
613
  * unaffected. Only the omitted case moves.
611
614
  */
612
- const bodyMode = 'boolean' === typeof args['bodies'] ? (args['bodies'] ? 'all' : 'none') : 'auto';
615
+ let bodyMode = 'auto';
616
+ if ('boolean' === typeof args['bodies']) {
617
+ bodyMode = args['bodies'] ? 'all' : 'none';
618
+ }
613
619
  // A filter names specific calls, so under `auto` their bodies are the thing being asked about.
614
620
  const named = method !== undefined || urlContains !== undefined || status !== undefined;
615
621
  const buffer = bufferEnvelope(session);
@@ -729,7 +735,7 @@ export const OBSERVE_TOOLS = [
729
735
  }
730
736
  // Default the cap so an omitted `limit` can't dump a whole flooded session (since defaults to 0).
731
737
  const { events: budgeted, droppedOldest } = applyEventBudget(matched, limit ?? DEFAULT_QUERY_LIMIT);
732
- const logs = budgeted.map(projectConsoleLog);
738
+ const logs = budgeted.map((e) => withPreAttach(projectConsoleLog(e), e));
733
739
  // Say the look HAPPENED when it found nothing. A quiet page and a dead console observer both
734
740
  // produced `{ logs: [] }`, and "no console errors" is the claim agents lean on most — see
735
741
  // observed-nothing.ts, whose own header names this exact case and which every other array-
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Say on a console line that the page raised it before the SDK attached (#1540).
3
+ *
4
+ * The build plugin's inline buffer holds what an app raised while nothing listened: an entry module
5
+ * that 404'd or threw, a rejection, a `console.error` during start-up. The SDK reports it when it
6
+ * attaches, marked `preAttach`. Without the mark on the line the agent reads, a reported error looks
7
+ * like one the last action caused, and the reason a page is blank is not read as a load failure.
8
+ *
9
+ * Here and not in the engine's `projectConsoleLog`: the engine does not take new names from core.
10
+ */
11
+ import { type ReticleEvent } from '@reticlehq/core';
12
+ export interface PreAttachMark {
13
+ /** The page raised it before the SDK attached. */
14
+ preAttach?: true;
15
+ /** How many more pre-attach errors the full buffer did not keep — loss, declared. */
16
+ droppedPreAttach?: number;
17
+ }
18
+ /** The console line, with the pre-attach mark and the declared loss when the event carries them. */
19
+ export declare function withPreAttach<T extends object>(view: T, e: ReticleEvent): T & PreAttachMark;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Say on a console line that the page raised it before the SDK attached (#1540).
3
+ *
4
+ * The build plugin's inline buffer holds what an app raised while nothing listened: an entry module
5
+ * that 404'd or threw, a rejection, a `console.error` during start-up. The SDK reports it when it
6
+ * attaches, marked `preAttach`. Without the mark on the line the agent reads, a reported error looks
7
+ * like one the last action caused, and the reason a page is blank is not read as a load failure.
8
+ *
9
+ * Here and not in the engine's `projectConsoleLog`: the engine does not take new names from core.
10
+ */
11
+ import { CONSOLE_EVENT, asNumber } from '@reticlehq/core';
12
+ /** The console line, with the pre-attach mark and the declared loss when the event carries them. */
13
+ export function withPreAttach(view, e) {
14
+ const mark = {};
15
+ if (true === e.data[CONSOLE_EVENT.PRE_ATTACH]) {
16
+ mark.preAttach = true;
17
+ }
18
+ const dropped = asNumber(e.data[CONSOLE_EVENT.DROPPED_PRE_ATTACH]);
19
+ if (dropped !== undefined && dropped > 0) {
20
+ mark.droppedPreAttach = dropped;
21
+ }
22
+ return { ...view, ...mark };
23
+ }
@@ -2,12 +2,14 @@
2
2
  * Read / record / replay tools — baselines + diff, recordings + replay, narrate, clock, state,
3
3
  * explore. Split out of tools.ts; assembled back via...READ_TOOLS.
4
4
  */
5
- import { UNKEPT_EXPECT } from '../../language/flows/replay.js';
5
+ import { UNKEPT_EXPECT, pageOf } from '../../language/flows/replay.js';
6
6
  import { resolveAnnotateTarget } from '../../language/flows/annotate-notes/annotate-target.js';
7
7
  import { z } from 'zod';
8
8
  import { EventType, ReticleCommand, REPLAY_PROGRAM_VERSION, SnapshotMode, StorageArea, } from '@reticlehq/core';
9
9
  import { ReticleTool } from '@reticlehq/core';
10
10
  import { advanceMsSchema, depthSchema } from './args/numeric-bounds.js';
11
+ import { StepFramesSchema } from '../../portal/pool/determinism.js';
12
+ const NOT_DETERMINISTIC = 'stepFrames needs a deterministic lease. Acquire one with reticle_lease { action: "acquire", url, deterministic: {} }, then pass its sessionId.';
11
13
  import { aliasParam, aliasUsed } from './args/alias-args.js';
12
14
  /** `name` for `recordingName`: the spelling every other naming argument uses. */
13
15
  const RECORDING_NAME_ALIAS = z.string().optional().describe('Alias for `recordingName`.');
@@ -22,25 +24,6 @@ import { buildReactionReport, summarizeReaction } from '@reticlehq/engine/questi
22
24
  import { asNumber, asString, parseInteractive } from '@reticlehq/core';
23
25
  import { sessionIdShape, commandOrThrow, snapshotTree } from './tool-kit.js';
24
26
  import { bufferEnvelope } from '../../portal/session/session-health.js';
25
- import { navigablePath, routeOfUrl } from '@reticlehq/engine/question/predicate/predicate-route.js';
26
- /** The route part of a session URL. A host belongs to the machine, not to the journey. */
27
- /**
28
- * The page a recording started on, as a NAVIGABLE path: pathname + query + hash.
29
- *
30
- * The hash matters and the pathname alone is not enough. Under a hash router every page has the
31
- * document pathname `/`, so a recording made on `#/posts/12` stored `/`, the replay's start-path
32
- * comparison saw `/` on both sides and never warned about drift, and the navigate it suggests
33
- * pointed at the default route rather than the recorded one.
34
- */
35
- function pathnameOf(url) {
36
- if (url === undefined)
37
- return undefined;
38
- const parts = routeOfUrl(url);
39
- // The query too, minus Reticle's own params: `/products?scope=TOPWEAR` and `/products` are different
40
- // pages, and replay compares the query when the flow recorded one (#1059), so dropping it here
41
- // started the replay on the unfiltered page (#1411).
42
- return parts === undefined ? undefined : navigablePath(parts);
43
- }
44
27
  /** What record-stop says when the span crossed a full-page load (#1411). */
45
28
  const RECORDING_CROSSED_RELOAD = 'The recording crossed a full-page load: the page it started on unloaded, so the reaction report and the proposed consequences cover only the page that loaded after it.';
46
29
  /**
@@ -161,7 +144,7 @@ export const READ_TOOLS = [
161
144
  recordingName: z
162
145
  .string()
163
146
  .optional()
164
- .describe('Identifier for this recording. Pass the same name to reticle_record{action:"stop"} and reticle_replay.'),
147
+ .describe('Identifier for this recording. Pass the same name to reticle_record{action:"stop"} and reticle_record{action:"replay"}.'),
165
148
  name: RECORDING_NAME_ALIAS,
166
149
  ...sessionIdShape,
167
150
  },
@@ -176,9 +159,9 @@ export const READ_TOOLS = [
176
159
  const name = asString(aliasParam(args, 'recordingName', ['name'])['recordingName']) ?? 'default';
177
160
  const cursor = session.elapsed();
178
161
  // Where the journey begins, so a saved flow can navigate here before step 1 instead of
179
- // replaying from wherever the page happens to be. Pathname only: a host or port belongs to
180
- // the machine that recorded it, not to the journey.
181
- deps.recordings.start(name, cursor, pathnameOf(session.url), session.id, session.currentDocumentId);
162
+ // replaying from wherever the page happens to be. No host or port: they belong to the machine
163
+ // that recorded it, not to the journey. The app's query and hash stay (see pageOf).
164
+ deps.recordings.start(name, cursor, pageOf(session.url), session.id, session.currentDocumentId);
182
165
  return Promise.resolve({
183
166
  recordingName: name,
184
167
  since: cursor,
@@ -339,7 +322,7 @@ export const READ_TOOLS = [
339
322
  },
340
323
  {
341
324
  name: ReticleTool.CLOCK,
342
- description: 'Control a fake clock: { freeze:true } to freeze time, { advanceMs:N } to fast-forward timers (toasts, debounces, auto-dismiss), { reset:true } to restore. Lets you test time-gated UI deterministically.',
325
+ description: 'Control a fake clock: { freeze:true } to freeze time, { advanceMs:N } to fast-forward timers (toasts, debounces, auto-dismiss), { reset:true } to restore. Lets you test time-gated UI deterministically. On a lease acquired with `deterministic`, { stepFrames:N } moves its clock N frames of 16.67 ms: due timers run, then the requestAnimationFrame callbacks, once each frame.',
343
326
  inputSchema: {
344
327
  freeze: z
345
328
  .boolean()
@@ -349,6 +332,7 @@ export const READ_TOOLS = [
349
332
  .optional()
350
333
  .describe('Fast-forward time by this many milliseconds — triggers debounces, toasts, auto-dismiss timers.'),
351
334
  reset: z.boolean().optional().describe('Restore the real clock.'),
335
+ stepFrames: StepFramesSchema.optional().describe('Step a deterministic lease by this many frames (60 each second). Only for a lease acquired with `deterministic`.'),
352
336
  ...sessionIdShape,
353
337
  },
354
338
  // The command returns `{ frozen }`. It used to declare `{ ok, elapsed }` — neither of which any
@@ -356,12 +340,31 @@ export const READ_TOOLS = [
356
340
  // successful freeze and a failed one both validated to `{}` and became indistinguishable.
357
341
  outputSchema: {
358
342
  frozen: z.boolean().optional(),
343
+ frame: z
344
+ .number()
345
+ .optional()
346
+ .describe('After stepFrames: the frames stepped since the page loaded.'),
347
+ nowMs: z
348
+ .number()
349
+ .optional()
350
+ .describe("After stepFrames: the page's performance.now(), in virtual milliseconds."),
351
+ },
352
+ handler: async (deps, args) => {
353
+ if (args['stepFrames'] !== undefined) {
354
+ const session = deps.sessions.resolve(asString(args['sessionId']));
355
+ const frames = StepFramesSchema.parse(args['stepFrames']);
356
+ const stepped = await deps.pool?.stepLease(session.id, frames);
357
+ if (stepped === undefined) {
358
+ throw new Error(NOT_DETERMINISTIC);
359
+ }
360
+ return { frozen: true, ...stepped };
361
+ }
362
+ return commandOrThrow(deps, asString(args['sessionId']), ReticleCommand.CLOCK, {
363
+ freeze: args['freeze'],
364
+ advanceMs: args['advanceMs'],
365
+ reset: args['reset'],
366
+ });
359
367
  },
360
- handler: (deps, args) => commandOrThrow(deps, asString(args['sessionId']), ReticleCommand.CLOCK, {
361
- freeze: args['freeze'],
362
- advanceMs: args['advanceMs'],
363
- reset: args['reset'],
364
- }),
365
368
  },
366
369
  {
367
370
  name: ReticleTool.STATE,
@@ -62,13 +62,5 @@ export declare const HOVER_OFF_VIEWPORT_MSG = "cannot hover: the target is outsi
62
62
  * or throws.
63
63
  */
64
64
  export declare function tryRealInput(deps: ToolDeps, session: Session, ref: string, action: ActionType, args: Record<string, unknown>): Promise<RealActResult>;
65
- /**
66
- * The session a flow replay runs on, with a press sent to the lease's real keyboard.
67
- *
68
- * Replay dispatches its steps itself, not through `reticle_act`, so a recorded Tab replayed as a
69
- * synthetic keydown that moves no focus, and a keyboard flow that passed live drifted on replay.
70
- * Every other command, and a press this lease cannot drive for real, goes to the page as before.
71
- * A Proxy, not a copy: the session keeps private state its own methods read.
72
- */
73
65
  export declare function withLeaseKeyboard(deps: ToolDeps, session: Session): Session;
74
66
  export {};
@@ -19,14 +19,15 @@
19
19
  * the base64 4/3 inflation factor applied, so the encoded payload always fits in one WebSocket
20
20
  * frame.
21
21
  */
22
- import { ActionType, clickInputProblem, DriveErrorCode, ElementState, INSPECT_IN_VIEWPORT_FIELD, INSPECT_REVEAL_ARG, InputMode, InputModeReason, ReticleCommand, TRANSPORT_LIMITS, } from '@reticlehq/core';
23
- import { boxCenter, DriveError, isRealInputAction, unspellablePressReason, } from '../../portal/input/real-input.js';
22
+ import { ACT_POINT_ARG, ActionType, actPointProblem, boxFractionFromArgs, clickInputProblem, DRAG_TO_POINT_ARG, DriveErrorCode, ElementState, INSPECT_IN_VIEWPORT_FIELD, INSPECT_REVEAL_ARG, InputMode, InputModeReason, ReticleCommand, TRANSPORT_LIMITS, } from '@reticlehq/core';
23
+ import { boxPoint, DriveError, isRealInputAction, unspellablePressReason, } from '../../portal/input/real-input.js';
24
24
  import { assertDragNotDestructive, assertNotDestructive } from './act/act-danger.js';
25
25
  import { assertNativeAllowed } from '../../portal/production/production-drive.js';
26
26
  import { NATIVE_INPUT_ARG } from '@reticlehq/core';
27
27
  import { asNumber, asRecord, asString } from '@reticlehq/core';
28
28
  import { commandOrThrow } from './tool-kit.js';
29
29
  import { asBox } from './act/act-helpers.js';
30
+ import { isStaleRefError } from './act/act-sequence-retry.js';
30
31
  import { isAbsolute, join, relative, extname, basename, resolve } from 'node:path';
31
32
  import { homedir } from 'node:os';
32
33
  import { RETICLE_CONFIG_BASENAME } from '../../command/cli/ports/resolve/cli-port.js';
@@ -269,6 +270,15 @@ function performArgsOf(inner) {
269
270
  if (holdMs !== undefined) {
270
271
  performArgs.holdMs = holdMs;
271
272
  }
273
+ // A point inside the target: a canvas has no child elements, so its centre is not enough.
274
+ const at = boxFractionFromArgs(inner, ACT_POINT_ARG);
275
+ if (at !== undefined) {
276
+ performArgs.at = at;
277
+ }
278
+ const to = boxFractionFromArgs(inner, DRAG_TO_POINT_ARG);
279
+ if (to !== undefined) {
280
+ performArgs.to = to;
281
+ }
272
282
  // `keys` and `code` are read but not forwarded: both are routed away by the caller — a multi-key
273
283
  // sequence and an explicit physical code have no driver spelling — so carrying them into the
274
284
  // provider would only invite it to guess at a gesture the caller did not ask for.
@@ -284,7 +294,11 @@ const ANCHOR_FIELDS = [
284
294
  'name',
285
295
  'nth',
286
296
  'of',
297
+ 'componentNth',
298
+ 'componentOf',
299
+ 'ownText',
287
300
  'secret',
301
+ 'within',
288
302
  ];
289
303
  /**
290
304
  * Press with a leased page's real keyboard, or undefined when this session is not such a lease.
@@ -298,6 +312,31 @@ const ANCHOR_FIELDS = [
298
312
  * keeps the in-page press, with the reason: a person's keyboard could not reach it either.
299
313
  * ponytail: lease only; the drive provider keeps its documented element-press rule.
300
314
  */
315
+ /**
316
+ * Put focus on a press's target, as a person does before they press a key in a field.
317
+ *
318
+ * A real keyboard cannot aim at an element, but focus can: focus the element, confirm it holds
319
+ * focus, and the real key then lands there, with every default action a browser gives a trusted
320
+ * key (an Enter in a text field submits its form). Returns what the focused element is called, so
321
+ * the saved step can find it again (the real keyboard reports no element of its own), or
322
+ * `undefined` when the element did not take focus. No ref: nothing to focus, nothing to name.
323
+ */
324
+ async function focusForPress(deps, session, ref) {
325
+ if (0 === ref.length) {
326
+ return {};
327
+ }
328
+ const focus = asRecord(await commandOrThrow(deps, session.id, ReticleCommand.ACT, {
329
+ ref,
330
+ action: ActionType.FOCUS,
331
+ args: {},
332
+ }));
333
+ const inspected = asRecord(await commandOrThrow(deps, session.id, ReticleCommand.INSPECT, { ref }));
334
+ const states = Array.isArray(inspected['states']) ? inspected['states'] : [];
335
+ if (!states.includes(ElementState.FOCUSED)) {
336
+ return undefined;
337
+ }
338
+ return Object.fromEntries(ANCHOR_FIELDS.flatMap((k) => (k in focus ? [[k, focus[k]]] : [])));
339
+ }
301
340
  async function pressOnLease(deps, session, ref, inner) {
302
341
  const pool = deps.pool;
303
342
  if (pool === undefined || !pool.hasKeyboard(session.id)) {
@@ -307,21 +346,9 @@ async function pressOnLease(deps, session, ref, inner) {
307
346
  if (unspellable !== undefined) {
308
347
  return synthetic(unspellable);
309
348
  }
310
- // What the focused element is called, from the focus act's own report, so the saved step can
311
- // find it again: the real keyboard reports no element of its own.
312
- let anchor = {};
313
- if (0 < ref.length) {
314
- const focus = asRecord(await commandOrThrow(deps, session.id, ReticleCommand.ACT, {
315
- ref,
316
- action: ActionType.FOCUS,
317
- args: {},
318
- }));
319
- anchor = Object.fromEntries(ANCHOR_FIELDS.flatMap((k) => (k in focus ? [[k, focus[k]]] : [])));
320
- const inspected = asRecord(await commandOrThrow(deps, session.id, ReticleCommand.INSPECT, { ref }));
321
- const states = Array.isArray(inspected['states']) ? inspected['states'] : [];
322
- if (!states.includes(ElementState.FOCUSED)) {
323
- return synthetic(InputModeReason.TARGET_NOT_FOCUSABLE);
324
- }
349
+ const anchor = await focusForPress(deps, session, ref);
350
+ if (anchor === undefined) {
351
+ return synthetic(InputModeReason.TARGET_NOT_FOCUSABLE);
325
352
  }
326
353
  const performed = await pool.pressLease(session.id, performArgsOf(inner));
327
354
  if (performed?.performed !== true) {
@@ -403,20 +430,21 @@ async function hoverForReal(deps, session, ref, inner, provider) {
403
430
  if (!inView) {
404
431
  throw new Error(HOVER_OFF_VIEWPORT_MSG);
405
432
  }
433
+ const at = boxFractionFromArgs(inner, ACT_POINT_ARG);
406
434
  if (providerReady && provider !== undefined) {
407
435
  try {
408
- const performed = await provider.perform(session.url, ActionType.HOVER, box, {});
436
+ const performed = await provider.perform(session.url, ActionType.HOVER, box, at === undefined ? {} : { at });
409
437
  // Hover always has a resolved box, so there is always a center to report: the provider's own
410
438
  // reading, or the box's if it reported none. Both are the same point, not a fabricated one.
411
439
  if (performed.performed) {
412
- return hoverSucceeded(performed.center ?? boxCenter(box), scrolled);
440
+ return hoverSucceeded(performed.center ?? boxPoint(box, at), scrolled);
413
441
  }
414
442
  }
415
443
  catch {
416
444
  // A provider error used to fall back to synthetic dispatch. For hover that is a lie.
417
445
  }
418
446
  }
419
- const { cx, cy } = boxCenter(box);
447
+ const { cx, cy } = boxPoint(box, at);
420
448
  if (true === (await deps.pool?.hoverLease(session.id, cx, cy))) {
421
449
  return hoverSucceeded({ cx, cy }, scrolled);
422
450
  }
@@ -436,6 +464,11 @@ export async function tryRealInput(deps, session, ref, action, args) {
436
464
  const provider = deps.realInput;
437
465
  const inner = asRecord(args['args']);
438
466
  const askedForNative = true === inner[NATIVE_INPUT_ARG];
467
+ // The page refuses the same point on the synthetic path, with the same sentence.
468
+ const pointProblem = actPointProblem(action, inner);
469
+ if (pointProblem !== undefined) {
470
+ throw new Error(`cannot ${action}: ${pointProblem}`);
471
+ }
439
472
  if (action === ActionType.HOVER) {
440
473
  return hoverForReal(deps, session, ref, inner, provider);
441
474
  }
@@ -460,8 +493,10 @@ export async function tryRealInput(deps, session, ref, action, args) {
460
493
  }
461
494
  // Two `press`es this module will NOT drive, and neither is a fallback — the reason says which
462
495
  // one was asked for, instead of a different gesture being sent. See `unspellablePressReason`.
496
+ // A press aimed at an element is focused first (focusForPress), as on a lease; only a chord and
497
+ // an explicit key code stay in the page.
463
498
  if (action === ActionType.PRESS) {
464
- const unspellable = unspellablePressReason(ref, inner);
499
+ const unspellable = unspellablePressReason('', inner);
465
500
  if (unspellable !== undefined) {
466
501
  return synthetic(unspellable);
467
502
  }
@@ -500,8 +535,12 @@ export async function tryRealInput(deps, session, ref, action, args) {
500
535
  return synthetic(InputModeReason.PAGE_NOT_CORRELATED);
501
536
  }
502
537
  const performArgs = performArgsOf(inner);
503
- // A document key has no element of its own: skip INSPECT and the box entirely.
538
+ // A key addresses focus, not a point: focus the target (if any), then skip INSPECT and the box.
504
539
  if (action === ActionType.PRESS) {
540
+ const anchor = await focusForPress(deps, session, ref);
541
+ if (anchor === undefined) {
542
+ return synthetic(InputModeReason.TARGET_NOT_FOCUSABLE);
543
+ }
505
544
  try {
506
545
  const performed = await provider.perform(session.url, action, PRESS_HAS_NO_BOX, performArgs);
507
546
  if (!performed.performed) {
@@ -516,6 +555,7 @@ export async function tryRealInput(deps, session, ref, action, args) {
516
555
  // `holdMs` to — an agent checking for an armed hold-to-confirm control read `undefined`.
517
556
  return {
518
557
  result: {
558
+ ...anchor,
519
559
  performed: true,
520
560
  action,
521
561
  inputMode: performed.inputMode,
@@ -557,8 +597,12 @@ export async function tryRealInput(deps, session, ref, action, args) {
557
597
  return synthetic(InputModeReason.ELEMENT_NOT_LOCATABLE);
558
598
  }
559
599
  let toBox;
560
- if (action === ActionType.DRAG) {
561
- const toRef = asString(inner['toRef']);
600
+ const toRef = asString(inner['toRef']);
601
+ // A drag that names only its end point moves inside the source, such as a stroke on one canvas.
602
+ if (action === ActionType.DRAG && toRef === undefined && performArgs.to !== undefined) {
603
+ toBox = box;
604
+ }
605
+ else if (action === ActionType.DRAG) {
562
606
  if (toRef === undefined) {
563
607
  return synthetic(InputModeReason.DRAG_TARGET_UNRESOLVED);
564
608
  }
@@ -614,18 +658,89 @@ export async function tryRealInput(deps, session, ref, action, args) {
614
658
  }
615
659
  }
616
660
  /**
617
- * The session a flow replay runs on, with a press sent to the lease's real keyboard.
661
+ * The session a flow replay runs on, with a press sent to the real keyboard, as `reticle_act` does.
618
662
  *
619
663
  * Replay dispatches its steps itself, not through `reticle_act`, so a recorded Tab replayed as a
620
664
  * synthetic keydown that moves no focus, and a keyboard flow that passed live drifted on replay.
621
- * Every other command, and a press this lease cannot drive for real, goes to the page as before.
622
- * A Proxy, not a copy: the session keeps private state its own methods read.
665
+ * The real keyboard is a lease's, or the driven browser's when a real-input provider is configured:
666
+ * that is `reticle verify`'s own page, where a synthetic Enter submitted no form, so a recorded
667
+ * "fill, press Enter" journey failed on a working app. Every other command, and a press that cannot
668
+ * be driven for real, goes to the page as before. A Proxy, not a copy: the session keeps private
669
+ * state its own methods read.
670
+ */
671
+ /**
672
+ * A sequence with a press in it, run as the page and the keyboard would run it.
673
+ *
674
+ * Replay sends a recorded sequence as one in-page command, so a press inside it stayed a synthetic
675
+ * key event: a recorded "fill, press Enter" sequence submitted no form and failed on a working app.
676
+ * The other sub-steps still go to the page together, in order; each press goes to the real keyboard
677
+ * (focus, then the key), and to the page as before when the real keyboard cannot take it. The
678
+ * first failure is returned as the page returned it, so a stale-ref retry still finds its step.
623
679
  */
680
+ async function sequenceWithRealPresses(deps, session, steps, timeoutMs) {
681
+ const share = (n) => timeoutMs === undefined ? undefined : Math.ceil((timeoutMs * n) / Math.max(1, steps.length));
682
+ let last = {
683
+ kind: 'command_result',
684
+ id: ReticleCommand.ACT_SEQUENCE,
685
+ ok: true,
686
+ result: {},
687
+ };
688
+ let chunk = [];
689
+ const flush = async () => {
690
+ if (0 === chunk.length) {
691
+ return true;
692
+ }
693
+ last = await session.command(ReticleCommand.ACT_SEQUENCE, { steps: chunk }, share(chunk.length));
694
+ chunk = [];
695
+ return last.ok;
696
+ };
697
+ for (const sub of steps) {
698
+ const step = asRecord(sub);
699
+ if (ActionType.PRESS !== step['action']) {
700
+ chunk.push(sub);
701
+ continue;
702
+ }
703
+ if (!(await flush())) {
704
+ return last;
705
+ }
706
+ // A click before this press can load a new page, and then the press's ref is stale. Answered as
707
+ // the page answers a stale ref, so the caller's retry finds the press again on the new page.
708
+ let real;
709
+ try {
710
+ real = await tryRealInput(deps, session, asString(step['ref']) ?? '', ActionType.PRESS, step);
711
+ }
712
+ catch (error) {
713
+ const message = error instanceof Error ? error.message : String(error);
714
+ if (!isStaleRefError(message)) {
715
+ throw error;
716
+ }
717
+ return { kind: 'command_result', id: ActionType.PRESS, ok: false, error: message };
718
+ }
719
+ if (real.result === undefined) {
720
+ chunk.push(sub);
721
+ continue;
722
+ }
723
+ last = {
724
+ kind: 'command_result',
725
+ id: ActionType.PRESS,
726
+ ok: true,
727
+ result: { dispatched: true, settled: real.settled, ...asRecord(real.result) },
728
+ };
729
+ }
730
+ await flush();
731
+ return last;
732
+ }
624
733
  export function withLeaseKeyboard(deps, session) {
625
- if (deps.realInput !== undefined || deps.pool?.hasKeyboard(session.id) !== true) {
734
+ if (deps.realInput === undefined && deps.pool?.hasKeyboard(session.id) !== true) {
626
735
  return session;
627
736
  }
628
737
  const command = async (name, args = {}, timeoutMs) => {
738
+ const steps = args['steps'];
739
+ if (ReticleCommand.ACT_SEQUENCE === name &&
740
+ Array.isArray(steps) &&
741
+ steps.some((sub) => ActionType.PRESS === asRecord(sub)['action'])) {
742
+ return await sequenceWithRealPresses(deps, session, steps, timeoutMs);
743
+ }
629
744
  if (ReticleCommand.ACT === name && ActionType.PRESS === args['action']) {
630
745
  const ref = asString(args['ref']) ?? '';
631
746
  const real = await tryRealInput(deps, session, ref, ActionType.PRESS, args);
@@ -55,3 +55,17 @@ export declare function suiteFixturePort(realInput: RealInputProvider | undefine
55
55
  * is the reach the boundary guard exists to ask about.
56
56
  */
57
57
  export declare function sessionMutationPort(realInput: RealInputProvider | undefined, appUrl: string | undefined): Promise<NetworkMutationPort | undefined>;
58
+ /**
59
+ * The state a verify run started in, put back before each flow after the first.
60
+ *
61
+ * `reticle verify` replays every flow in ONE driven page. A flow that signs out (an auto-saved
62
+ * "log out" fragment, or a journey that ends on the logout link) left every later flow signed out,
63
+ * and each of them then failed for a reason none of them named. The leased suite path never had
64
+ * this: each flow boots its own context from one captured seed. This gives the single-page path
65
+ * the same start.
66
+ *
67
+ * Call it before each flow. It answers `undefined` to go on, or the reason the flow cannot start
68
+ * where the run started, which the caller reports as not proved. Nothing to capture (no driven
69
+ * page, no storage support) means the run goes on as before.
70
+ */
71
+ export declare function flowStartState(provider: RealInputProvider | undefined, currentUrl: () => string | undefined): () => Promise<string | undefined>;
@@ -100,3 +100,56 @@ export async function sessionMutationPort(realInput, appUrl) {
100
100
  return undefined;
101
101
  }
102
102
  }
103
+ /**
104
+ * The state a verify run started in, put back before each flow after the first.
105
+ *
106
+ * `reticle verify` replays every flow in ONE driven page. A flow that signs out (an auto-saved
107
+ * "log out" fragment, or a journey that ends on the logout link) left every later flow signed out,
108
+ * and each of them then failed for a reason none of them named. The leased suite path never had
109
+ * this: each flow boots its own context from one captured seed. This gives the single-page path
110
+ * the same start.
111
+ *
112
+ * Call it before each flow. It answers `undefined` to go on, or the reason the flow cannot start
113
+ * where the run started, which the caller reports as not proved. Nothing to capture (no driven
114
+ * page, no storage support) means the run goes on as before.
115
+ */
116
+ export function flowStartState(provider, currentUrl) {
117
+ let first = true;
118
+ let saved;
119
+ return async () => {
120
+ const url = currentUrl();
121
+ if (first) {
122
+ first = false;
123
+ if (provider === undefined || url === undefined) {
124
+ return undefined;
125
+ }
126
+ try {
127
+ const port = await fixturePortFor(provider, url);
128
+ saved = port === undefined ? undefined : { state: await port.capture(), url };
129
+ }
130
+ catch {
131
+ saved = undefined;
132
+ }
133
+ return undefined;
134
+ }
135
+ if (saved === undefined || provider === undefined) {
136
+ return undefined;
137
+ }
138
+ try {
139
+ // Straight to the provider, not through `fixturePortFor`: its address check compares the
140
+ // session's recorded address with the page's, and a flow that reloaded or signed out leaves
141
+ // the two different while the page is the same. The provider still refuses a page it does
142
+ // not drive (it answers false), so nothing is restored into a stranger's page.
143
+ // Tabs first: a tab the previous flow opened is a second connected session otherwise.
144
+ await provider.closeOtherPages?.();
145
+ const restored = await provider.applyStorageState?.(url ?? saved.url, saved.state);
146
+ return true === restored
147
+ ? undefined
148
+ : 'could not put the page back in the state the run started with: no driven page matched';
149
+ }
150
+ catch (error) {
151
+ const message = error instanceof Error ? error.message : String(error);
152
+ return `could not put the page back in the state the run started with: ${message}`;
153
+ }
154
+ };
155
+ }
@@ -31,6 +31,12 @@ export interface ToolDeps<Ext = unknown> {
31
31
  * the fake-pool lease tests are deliberately runnable without a real Chromium.
32
32
  */
33
33
  browserProbe?: () => Promise<ChromiumProbe>;
34
+ /**
35
+ * Whether something on this loopback port accepted a connection. Asked only after a lease
36
+ * navigation timed out, to tell a dev server compiling its first route from no server (#2005).
37
+ * Optional: absent ⇒ the timeout keeps its generic advice.
38
+ */
39
+ portAccepts?: (port: number) => Promise<boolean>;
34
40
  baselines: BaselineStore;
35
41
  recordings: RecordingStore;
36
42
  /** on-disk anchored-flow store (.reticle/flows/). */
@@ -156,6 +156,8 @@ export const CDP_TOOLS = new Set([
156
156
  // Breaks a request through the same Playwright route `reticle_network_mock` uses, so it fails the
157
157
  // same way under version skew and must be refused with the same sentence.
158
158
  ReticleTool.FLOW_MUTATE,
159
+ // Reads JS coverage through the lease's Playwright page, the way `reticle_network_mock` reaches it.
160
+ ReticleTool.LEASE_COVERAGE,
159
161
  ]);
160
162
  /**
161
163
  * Tools whose result hands the agent refs. The tab they were minted in is remembered, so the call
@@ -171,7 +173,6 @@ export const REF_MINTING_TOOLS = new Set([
171
173
  // The same three merged, on the `merged` surface. Its `state` action mints nothing, and
172
174
  // over-inclusion is the safe direction here: the worst case is a refusal naming two tabs.
173
175
  ReticleTool.LOOK,
174
- ReticleTool.EXPLORE,
175
176
  // `crawl` mints refs for the controls it drives and `coverage` for the ones it reports untouched.
176
177
  // Both are actions on the merged verify tool now.
177
178
  ReticleTool.VERIFY,
@@ -212,11 +213,9 @@ export const SESSION_BOUND_TOOLS = new Set([
212
213
  ReticleTool.ANIMATIONS,
213
214
  ReticleTool.BASELINE, // merged save/list/diff — save+diff are live reads
214
215
  ReticleTool.RECORD, // merged start/stop — both live
215
- ReticleTool.REPLAY,
216
216
  ReticleTool.CLOCK,
217
217
  ReticleTool.STATE,
218
218
  ReticleTool.STORAGE,
219
- ReticleTool.EXPLORE,
220
219
  ReticleTool.SCROLL_TO,
221
220
  ReticleTool.NAVIGATE,
222
221
  ]);
@@ -473,7 +473,6 @@ const OPEN_WORLD = new Set([
473
473
  ReticleTool.NAVIGATE,
474
474
  ReticleTool.SESSION,
475
475
  ReticleTool.VERIFY,
476
- ReticleTool.EXPLORE,
477
476
  ReticleTool.FEEDBACK,
478
477
  ReticleTool.RUN,
479
478
  ]);