@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
@@ -0,0 +1,11 @@
1
+ import { type CompareCommandOptions } from '../../command/cli/two-builds/compare-command.js';
2
+ import type { ToolDef } from './tool-kit.js';
3
+ type RunCompare = (options: CompareCommandOptions) => Promise<{
4
+ code: number;
5
+ lines: string[];
6
+ }>;
7
+ type Sleep = (ms: number) => Promise<void>;
8
+ /** The compare tool. `run` and `sleep` are injected so a test needs no daemon. */
9
+ export declare function compareTool(run?: RunCompare, sleep?: Sleep): ToolDef;
10
+ export declare const COMPARE_TOOLS: ToolDef[];
11
+ export {};
@@ -0,0 +1,94 @@
1
+ /**
2
+ * `reticle_verify { action: "compare" }`: `reticle compare` for an agent whose tools are loaded.
3
+ *
4
+ * It runs the CLI's own compare (`runCompareCommand`) against this daemon, over the same loopback
5
+ * transport the CLI uses, so the answer is the `--json` object of `reticle compare` and nothing
6
+ * else. The leased tabs resolve their own project from the page, as every lease does. A compare takes minutes, longer than one tool call may wait, so within `wait` seconds it
7
+ * answers the result, or `{ status: "running", runId }` to poll with `runId`, as `explore` does.
8
+ */
9
+ import { randomUUID } from 'node:crypto';
10
+ import { z } from 'zod';
11
+ import { RETICLE_DEFAULT_PORT, ReticleTool } from '@reticlehq/core';
12
+ import { pairingToken, runCompareCommand, } from '../../command/cli/two-builds/compare-command.js';
13
+ const RUN_PREFIX = 'compare-';
14
+ const COMPARE_ACTION = 'compare';
15
+ const RUNNING = 'running';
16
+ /** Seconds one call waits for the result before it answers with a run id. */
17
+ const DEFAULT_WAIT_S = 50;
18
+ const MAX_WAIT_S = 240;
19
+ const MS_PER_S = 1000;
20
+ /** The most finished runs kept for polling. A run is read back once or twice, not for ever. */
21
+ const KEPT_RUNS = 8;
22
+ const MSG_NEEDS_URLS = 'compare needs `reference` and `candidate`, the urls of two running builds';
23
+ const MSG_UNKNOWN_RUN = 'no compare run has the id';
24
+ const asText = (value) => 'string' === typeof value && value.length > 0 ? value : undefined;
25
+ /** The JSON object the CLI printed, or the line itself when it is not one. */
26
+ function parsed(lines) {
27
+ const line = lines[0] ?? '';
28
+ try {
29
+ return JSON.parse(line);
30
+ }
31
+ catch {
32
+ return { error: line };
33
+ }
34
+ }
35
+ /** The compare tool. `run` and `sleep` are injected so a test needs no daemon. */
36
+ export function compareTool(run = runCompareCommand, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
37
+ const runs = new Map();
38
+ const answer = async (runId, result, waitS) => {
39
+ const timedOut = Symbol(RUNNING);
40
+ const first = await Promise.race([result, sleep(waitS * MS_PER_S).then(() => timedOut)]);
41
+ if (first !== timedOut) {
42
+ return first;
43
+ }
44
+ return { status: RUNNING, runId, next: { action: COMPARE_ACTION, runId } };
45
+ };
46
+ return {
47
+ name: ReticleTool.VERIFY_COMPARE,
48
+ description: 'Drive two running builds of one app the same way (for example main and a pull request preview) and list each difference in what users see and in the requests and console errors the app made. Nothing is installed in the app. One opinionated recipe: plan every action on the reference, drive it on the reference two times and the candidate one time, and ignore what changes between the two reference runs. With `base`, each difference is tagged regression, target-missed or changed, with a score. Answers the JSON of `reticle compare --json`: `verified` yes (same), no (different) or unknown (the run proves nothing). Within `wait` seconds the result, or { status: "running", runId } to poll with `runId`. DESTRUCTIVE: it presses every button it finds.',
49
+ inputSchema: {
50
+ reference: z.string().optional().describe('URL of the build that is right, e.g. main.'),
51
+ candidate: z.string().optional().describe('URL of the build to check.'),
52
+ base: z.string().optional().describe('URL of the build before the change.'),
53
+ runId: z
54
+ .string()
55
+ .optional()
56
+ .describe('The run id a compare call answered with; pass it to poll that run.'),
57
+ wait: z.number().min(0).max(MAX_WAIT_S).optional().describe('Seconds; default 50.'),
58
+ },
59
+ handler: async (deps, args) => {
60
+ const waitS = 'number' === typeof args['wait'] ? args['wait'] : DEFAULT_WAIT_S;
61
+ const polled = asText(args['runId']);
62
+ if (polled !== undefined) {
63
+ const result = runs.get(polled);
64
+ if (result === undefined) {
65
+ return { error: `${MSG_UNKNOWN_RUN} ${polled}` };
66
+ }
67
+ return answer(polled, result, waitS);
68
+ }
69
+ const reference = asText(args['reference']);
70
+ const candidate = asText(args['candidate']);
71
+ if (reference === undefined || candidate === undefined) {
72
+ return { error: MSG_NEEDS_URLS };
73
+ }
74
+ const base = asText(args['base']);
75
+ const token = pairingToken();
76
+ const runId = `${RUN_PREFIX}${randomUUID()}`;
77
+ const result = run({
78
+ reference,
79
+ candidate,
80
+ ...(base === undefined ? {} : { base }),
81
+ json: true,
82
+ port: deps.bridgePort ?? RETICLE_DEFAULT_PORT,
83
+ ...(token === undefined ? {} : { token }),
84
+ }).then((out) => parsed(out.lines));
85
+ runs.set(runId, result);
86
+ // ponytail: the oldest runs are dropped by count. Drop by age if a session polls very late.
87
+ for (const old of [...runs.keys()].slice(0, Math.max(0, runs.size - KEPT_RUNS))) {
88
+ runs.delete(old);
89
+ }
90
+ return answer(runId, result, waitS);
91
+ },
92
+ };
93
+ }
94
+ export const COMPARE_TOOLS = [compareTool()];
@@ -63,7 +63,7 @@ export function buildCoverageTools() {
63
63
  app: z
64
64
  .boolean()
65
65
  .optional()
66
- .describe('Also fold into the app-wide ledger; returns `app.levels`.'),
66
+ .describe('Also fold into the app-wide ledger; returns `app.levels`, `app.holes` and `app.findings`.'),
67
67
  ...sessionIdShape,
68
68
  },
69
69
  outputSchema: {
@@ -310,10 +310,17 @@ callable) {
310
310
  hint: `pass them inside args: reticle_run { tool, args: { ${strayTop.join(', ')}: … } } if the target tool declares them`,
311
311
  };
312
312
  }
313
- const name = 'string' === typeof args['tool'] ? args['tool'] : '';
314
- const given = 'object' === typeof args['args'] && args['args'] !== null
313
+ const asked = 'string' === typeof args['tool'] ? args['tool'] : '';
314
+ const passed = 'object' === typeof args['args'] && args['args'] !== null
315
315
  ? args['args']
316
316
  : {};
317
+ // A merged name is CALLED, as the MCP layer does (mcp.ts). Explaining it here left
318
+ // `reticle verify --expect` unable to wait, so it could never give a verdict. The old name is
319
+ // the action, so it wins over an `action` in the arguments.
320
+ const moved = byName.has(asked) ? undefined : mergedNameRedirect(asked);
321
+ const routed = moved?.action !== undefined && byName.has(moved.tool);
322
+ const name = routed ? moved.tool : asked;
323
+ const given = routed ? { ...passed, action: moved.action } : passed;
317
324
  const aimed = args['sessionId'];
318
325
  const target = byName.get(name);
319
326
  if (target === undefined) {
@@ -8,7 +8,7 @@
8
8
  * on stable, human-authored substrings of the thrown messages (see session-manager.ts, the flow/
9
9
  * baseline stores). No clock, no IO — unit-testable in isolation.
10
10
  */
11
- import { RefusalReason, type CodedRefusal } from '@reticlehq/core';
11
+ import { RefusalDetail, RefusalReason, type CodedRefusal } from '@reticlehq/core';
12
12
  /** The recovery hints, named so they are not free strings and can be asserted in tests. */
13
13
  export declare const RECOVERY: {
14
14
  readonly NO_SESSION: string;
@@ -97,6 +97,11 @@ export declare const RECOVERY: {
97
97
  * saying a server is running and the recovery saying to go start one.
98
98
  */
99
99
  export declare function recoveryFor(message: string): string | undefined;
100
+ /** A refusal's bucket, and the cause inside it when the recovery table can tell. */
101
+ export interface RefusalClass {
102
+ readonly reason: RefusalReason;
103
+ readonly detail?: RefusalDetail;
104
+ }
100
105
  /**
101
106
  * WHY a tool refused, as one of the six buckets in core's `RefusalReason`.
102
107
  *
@@ -106,6 +111,12 @@ export declare function recoveryFor(message: string): string | undefined;
106
111
  * this table rather than about anybody's app.
107
112
  */
108
113
  export declare function refusalReasonFor(rawMessage: string, coded?: CodedRefusal): RefusalReason;
114
+ /**
115
+ * The bucket and, when the recovery table names one, the cause inside it. The cause comes from the
116
+ * same rule that chose the agent's advice, so the two cannot disagree. No detail means no rule
117
+ * matched, or the message diagnosed itself (`noSessionReason` carries that cause).
118
+ */
119
+ export declare function classifyRefusal(rawMessage: string, coded?: CodedRefusal): RefusalClass;
109
120
  /**
110
121
  * The ask attached to errors we do NOT recognize. An unrecognized error is, by definition, the case
111
122
  * where Reticle failed in a way nobody anticipated — the single highest-value moment to hear from the
@@ -8,12 +8,13 @@
8
8
  * on stable, human-authored substrings of the thrown messages (see session-manager.ts, the flow/
9
9
  * baseline stores). No clock, no IO — unit-testable in isolation.
10
10
  */
11
- import { CodedRefusalSchema, PRODUCTION_REFUSAL_PREFIX, RefusalCode, RefusalReason, ReticleEnv, TRANSPORT_LIMITS, UNISSUED_REF_REFUSAL, } from '@reticlehq/core';
11
+ import { CodedRefusalSchema, PRODUCTION_REFUSAL_PREFIX, RefusalCode, RefusalDetail, RefusalReason, ReticleEnv, TRANSPORT_LIMITS, UNISSUED_REF_REFUSAL, } from '@reticlehq/core';
12
12
  import { SELF_RECOVERING_MARKER } from '../../portal/session/no-session-diagnosis.js';
13
13
  import { MSG_HARNESS_DISABLED, MSG_HARNESS_UNCLAIMED, MSG_HARNESS_UNCONFIRMED, PlatformRefusalCode, RefusalLead, } from '../../features/harness/platform/refusal.js';
14
14
  import { BODY_CLAUSE_REFUSAL_OPENING } from '@reticlehq/engine/evidence/body-capture-remedy.js';
15
15
  import { EMPTY_FIND_OPENING } from './read/query-shape.js';
16
16
  import { UNKNOWN_FLOW_OPENING } from '../../language/flows/suite-selection.js';
17
+ import { ARTIFACT_ROOT_ABSOLUTE_REFUSAL, ARTIFACT_ROOT_MISSING_REFUSAL, } from '../../memory/project/root-claims.js';
17
18
  import { CHROMIUM_PATH_REFUSAL_PREFIX, chromiumInstallCommand, bundledPlaywrightVersion, } from '../../command/cli/doctor/browser/chromium-hint.js';
18
19
  /** Marks where an over-long message had its middle removed. Named so tests assert it by value. */
19
20
  const ELISION = '… [elided] …';
@@ -71,6 +72,11 @@ const SELF_RECOVERING = [
71
72
  // Both are the caller's to fix, and the defect ask on top told an agent to file a bug about them.
72
73
  { match: literally(EMPTY_FIND_OPENING), reason: RefusalReason.BAD_ARGS },
73
74
  { match: literally(UNKNOWN_FLOW_OPENING), reason: RefusalReason.NO_MATCH },
75
+ // A lease `root` naming no directory on disk — relative, or absent — is the caller's to fix, and
76
+ // the message carries the exact path to pass instead. Both sat under the feedback ask, which
77
+ // invited a bug report about a mistyped path (#2085).
78
+ { match: literally(ARTIFACT_ROOT_ABSOLUTE_REFUSAL), reason: RefusalReason.BAD_ARGS },
79
+ { match: literally(ARTIFACT_ROOT_MISSING_REFUSAL), reason: RefusalReason.BAD_ARGS },
74
80
  ];
75
81
  /**
76
82
  * The refusal for a `ref` Reticle never minted — decided BEFORE every other rule, not by one inside
@@ -204,10 +210,12 @@ export const RECOVERY = {
204
210
  'on purpose. If you mean it, send the SAME call again with ' +
205
211
  '`args: { confirmDangerous: true }` — it goes inside the `args` object, not beside it. This is ' +
206
212
  'a deliberate refusal, not a defect: there is nothing to report.',
207
- UNSUPPORTED_SURFACE: 'That surface is not supported yet, and Reticle refuses rather than pretending — a rich-text ' +
208
- 'editor keeps its own document model, so writing to the DOM would look right and submit the old ' +
209
- 'content. Drive the same outcome another way (a plain input, a command, a keyboard action) or ' +
210
- 'assert on the result instead of typing it. Known gap, already reported: no need to file it.',
213
+ UNSUPPORTED_SURFACE: 'That surface is a rich-text editor: it keeps its own document model, so `fill` and `type` ' +
214
+ 'refuse it rather than write the DOM and submit the old content. A contenteditable editor ' +
215
+ '(Tiptap, ProseMirror) takes `insertText` with `{ text }`: it goes through the browser editing ' +
216
+ 'pipeline, so the editor model updates. An EditContext editor (Monaco, CodeMirror 6) cannot be ' +
217
+ 'typed into yet, because a scripted update fires no textupdate for its model: use `press` for ' +
218
+ 'a key. If the ref points at a wrapper, act on the editor inside it instead.',
211
219
  HOVER_NEEDS_POINTER: 'Hover needs a real pointer: a synthetic mouseover does not apply CSS :hover, and Reticle ' +
212
220
  'refuses rather than reporting the styles as applied. Acquire a tab with ' +
213
221
  'reticle_run { tool: "reticle_lease", args: { action: "acquire", url } } — reticle_lease is not ' +
@@ -259,7 +267,7 @@ export const RECOVERY = {
259
267
  'what is wrong with the path). The call itself was fine, so retrying it will not help, and ' +
260
268
  'neither will installing Chromium. Ask the human to fix that path, or unset it to use ' +
261
269
  "Playwright's own Chromium, in the environment the daemon starts from, then run " +
262
- '`npx @reticlehq/server restart`. Meanwhile drive a tab the human already has open, which ' +
270
+ '`npx @reticlehq/server serve --restart`. Meanwhile drive a tab the human already has open, which ' +
263
271
  'reticle_sessions lists. This is configuration, not a Reticle defect: there is nothing to report.',
264
272
  /**
265
273
  * The two ways a platform answer can refuse a drive. Neither is a fault, and both were telling the
@@ -325,6 +333,44 @@ const REASON_OF = {
325
333
  // The call is valid; the app is not instrumented enough for the Harness yet.
326
334
  HARNESS_LOCKED: RefusalReason.UNSUPPORTED,
327
335
  };
336
+ /**
337
+ * Which CAUSE each recovery names, for `tool_refused`. A `Record` for the same reason as `REASON_OF`:
338
+ * a recovery added without a detail does not compile. A rule can name a narrower detail than its
339
+ * recovery (`NO_POOL` covers three causes with one hint).
340
+ */
341
+ const DETAIL_OF = {
342
+ NO_SESSION: RefusalDetail.NO_SESSION,
343
+ MULTIPLE_SESSIONS: RefusalDetail.MULTIPLE_SESSIONS,
344
+ UNKNOWN_SESSION: RefusalDetail.UNKNOWN_SESSION,
345
+ SCOPE_MISMATCH: RefusalDetail.SCOPE_MISMATCH,
346
+ SESSION_GONE: RefusalDetail.SESSION_GONE,
347
+ THROTTLED: RefusalDetail.THROTTLED,
348
+ COMMAND_TIMEOUT: RefusalDetail.COMMAND_TIMEOUT,
349
+ NO_POOL: RefusalDetail.POOL_UNAVAILABLE,
350
+ CHROMIUM_PATH: RefusalDetail.CHROMIUM_PATH,
351
+ TOKEN_REQUIRED: RefusalDetail.TOKEN_REQUIRED,
352
+ MISSING_BASELINE: RefusalDetail.MISSING_BASELINE,
353
+ MISSING_RECORDING: RefusalDetail.MISSING_RECORDING,
354
+ STALE_REF: RefusalDetail.STALE_REF,
355
+ STALE_REF_AFTER_EDIT: RefusalDetail.STALE_REF_AFTER_EDIT,
356
+ UNISSUED_REF: RefusalDetail.UNISSUED_REF,
357
+ NO_SUCH_OPTION: RefusalDetail.NO_SUCH_OPTION,
358
+ TARGET_MISSED: RefusalDetail.TARGET_MISSED,
359
+ AMBIGUOUS_TARGET: RefusalDetail.AMBIGUOUS_TARGET,
360
+ FLOW_STEP_MISSING: RefusalDetail.FLOW_STEP_MISSING,
361
+ UNSUPPORTED_SURFACE: RefusalDetail.UNSUPPORTED_SURFACE,
362
+ HOVER_NEEDS_POINTER: RefusalDetail.HOVER_NEEDS_POINTER,
363
+ NOT_EDITABLE: RefusalDetail.NOT_EDITABLE,
364
+ CONFIRM_DANGEROUS: RefusalDetail.CONFIRM_DANGEROUS,
365
+ WRONG_TARGET: RefusalDetail.WRONG_TARGET,
366
+ PRODUCTION_POLICY: RefusalDetail.PRODUCTION_POLICY,
367
+ BAD_ARGUMENTS: RefusalDetail.BAD_ARGUMENTS,
368
+ INVALID_NAME: RefusalDetail.INVALID_NAME,
369
+ HARNESS_OFF: RefusalDetail.HARNESS_OFF,
370
+ HARNESS_LOCKED: RefusalDetail.HARNESS_LOCKED,
371
+ };
372
+ /** Hint text back to its detail, as `REASON_BY_HINT` does for the reason. */
373
+ const DETAIL_BY_HINT = new Map(Object.entries(RECOVERY).map(([key, hint]) => [hint, DETAIL_OF[key]]));
328
374
  /** Hint text back to its reason. The hints are distinct strings, so this inverts cleanly. */
329
375
  const REASON_BY_HINT = new Map(Object.entries(RECOVERY).map(([key, hint]) => [hint, REASON_OF[key]]));
330
376
  /** Ordered match rules; the first hit wins. Substrings track the thrown messages they recover. */
@@ -375,9 +421,17 @@ const RULES = [
375
421
  { match: /^command '[^']*' timed out after/i, hint: RECOVERY.COMMAND_TIMEOUT },
376
422
  // Flow replay: the recorded anchor is gone. Ordinary drift, and the tool that fixes it exists.
377
423
  { match: /did not resolve in current page/i, hint: RECOVERY.FLOW_STEP_MISSING },
424
+ // One hint for three causes: the agent's next step is the same, the fix for the user is not.
425
+ { match: /browser pool unavailable/i, hint: RECOVERY.NO_POOL },
378
426
  {
379
- match: /browser pool (?:unavailable|is shut down)|Chromium is not installed/i,
427
+ match: /browser pool is shut down/i,
380
428
  hint: RECOVERY.NO_POOL,
429
+ detail: RefusalDetail.POOL_SHUT_DOWN,
430
+ },
431
+ {
432
+ match: /Chromium is not installed/i,
433
+ hint: RECOVERY.NO_POOL,
434
+ detail: RefusalDetail.CHROMIUM_MISSING,
381
435
  },
382
436
  // The commonest post-action condition there is. Unmatched, it fell through to FEEDBACK_ASK and
383
437
  // told the agent a successful click's aftermath might be a bug in Reticle.
@@ -392,6 +446,10 @@ const RULES = [
392
446
  // broken. Order matters: contenteditable and disabled/readonly are `cannot <verb> …` messages too,
393
447
  // so they must be tested before the general wrong-target rule.
394
448
  { match: /contenteditable/i, hint: RECOVERY.UNSUPPORTED_SURFACE },
449
+ // The EditContext refusal from insertText. Its message names no `contenteditable`, so it used to
450
+ // fall through to the feedback ask — a deliberate refusal of a surface we cannot type into yet
451
+ // read as "Reticle may be broken".
452
+ { match: /EditContext/i, hint: RECOVERY.UNSUPPORTED_SURFACE },
395
453
  { match: /cannot hover without a real pointer/i, hint: RECOVERY.HOVER_NEEDS_POINTER },
396
454
  { match: /cannot \w+ a (disabled|readonly) </i, hint: RECOVERY.NOT_EDITABLE },
397
455
  // Both "action" and "native action" refusals need the same confirmation guidance.
@@ -473,12 +531,11 @@ export function recoveryFor(message) {
473
531
  if (isSelfRecovering(message)) {
474
532
  return undefined;
475
533
  }
476
- for (const rule of RULES) {
477
- if (rule.match.test(message)) {
478
- return rule.hint;
479
- }
480
- }
481
- return undefined;
534
+ return firstRule(message)?.hint;
535
+ }
536
+ /** The first rule that matches the message. The order of `RULES` decides. */
537
+ function firstRule(message) {
538
+ return RULES.find((rule) => rule.match.test(message));
482
539
  }
483
540
  /**
484
541
  * WHY a tool refused, as one of the six buckets in core's `RefusalReason`.
@@ -489,33 +546,46 @@ export function recoveryFor(message) {
489
546
  * this table rather than about anybody's app.
490
547
  */
491
548
  export function refusalReasonFor(rawMessage, coded) {
549
+ return classifyRefusal(rawMessage, coded).reason;
550
+ }
551
+ /**
552
+ * The bucket and, when the recovery table names one, the cause inside it. The cause comes from the
553
+ * same rule that chose the agent's advice, so the two cannot disagree. No detail means no rule
554
+ * matched, or the message diagnosed itself (`noSessionReason` carries that cause).
555
+ */
556
+ export function classifyRefusal(rawMessage, coded) {
492
557
  if (coded !== undefined) {
493
- return REASON_OF_CODE[coded.code];
558
+ return { reason: REASON_OF_CODE[coded.code] };
494
559
  }
495
560
  const message = zodArrayAsSentence(rawMessage);
496
561
  if (isUnissuedRefRefusal(message)) {
497
- return RefusalReason.BAD_ARGS;
562
+ return { reason: RefusalReason.BAD_ARGS, detail: RefusalDetail.UNISSUED_REF };
498
563
  }
499
564
  // The no-session diagnosis inspected the machine and named the cause itself, so it never reaches
500
565
  // the recovery table. It is the single largest refusal there is; missing it would gut the metric.
501
566
  if (message.includes(SELF_RECOVERING_MARKER)) {
502
- return RefusalReason.NO_SESSION;
567
+ return { reason: RefusalReason.NO_SESSION };
503
568
  }
504
569
  for (const rule of SELF_RECOVERING) {
505
570
  if (rule.match.test(message)) {
506
- return rule.reason;
571
+ return { reason: rule.reason };
507
572
  }
508
573
  }
509
574
  // Same order as buildErrorPayload: a schema rejection is the agent's own call, whatever else in
510
575
  // the message might also match.
511
576
  if (ARGUMENT_REJECTION.test(message)) {
512
- return RefusalReason.BAD_ARGS;
577
+ return { reason: RefusalReason.BAD_ARGS };
513
578
  }
514
579
  if (INVALID_NAME_REJECTION.test(message)) {
515
- return RefusalReason.BAD_ARGS;
580
+ return { reason: RefusalReason.BAD_ARGS, detail: RefusalDetail.INVALID_NAME };
581
+ }
582
+ const rule = firstRule(message);
583
+ if (rule === undefined) {
584
+ return { reason: RefusalReason.OTHER };
516
585
  }
517
- const hint = recoveryFor(message);
518
- return (hint === undefined ? undefined : REASON_BY_HINT.get(hint)) ?? RefusalReason.OTHER;
586
+ const reason = REASON_BY_HINT.get(rule.hint) ?? RefusalReason.OTHER;
587
+ const detail = rule.detail ?? DETAIL_BY_HINT.get(rule.hint);
588
+ return detail === undefined ? { reason } : { reason, detail };
519
589
  }
520
590
  /**
521
591
  * A schema rejection: the call named a parameter that does not exist, or gave one the wrong type.
@@ -60,7 +60,7 @@ export const EXPLORE_TOOLS = [
60
60
  runId: z
61
61
  .string()
62
62
  .optional()
63
- .describe('The run id an explore call answered with; pass it to poll that run.'),
63
+ .describe('The run id a call answered with; pass it to poll that run.'),
64
64
  planId: z.string().optional().describe('Run a plan saved on the dashboard, as it stands.'),
65
65
  stop: z.boolean().optional().describe('With runId: stop that Harness run.'),
66
66
  say: z.string().optional().describe('With runId: a note to the running Harness.'),
@@ -30,7 +30,7 @@ import { rewriteClosedAsSkew } from '../../command/version/version-skew.js';
30
30
  import { noteToolCall } from '../../command/daemon/lifetime/daemon-usefulness.js';
31
31
  import { bugsInResult, routeOf } from '../../telemetry/bug-found.js';
32
32
  import { noteToolServed, reportToolRefused } from '../../telemetry/tool-refused.js';
33
- import { buildErrorPayload, codedRefusalOf, refusalReasonFor } from './error-recovery.js';
33
+ import { buildErrorPayload, classifyRefusal, codedRefusalOf } from './error-recovery.js';
34
34
  import { resultIsError } from '../mcp/faults/mcp-is-error.js';
35
35
  import { verificationOf } from '../../telemetry/verification-of.js';
36
36
  import { emitBugFoundHook, emitVerdictHook } from '../../hooks/hook-emit.js';
@@ -140,7 +140,8 @@ function reportRefusal(toolName, args, message, error) {
140
140
  if (toolName === ReticleTool.RUN) {
141
141
  return;
142
142
  }
143
- reportToolRefused(toolName, refusalReasonFor(message, codedRefusalOf(error)), args);
143
+ const { reason, detail } = classifyRefusal(message, codedRefusalOf(error));
144
+ reportToolRefused(toolName, reason, args, detail);
144
145
  }
145
146
  /**
146
147
  * Which friction this result represents, counting the invitation if there is one.
@@ -0,0 +1,49 @@
1
+ /**
2
+ * `reticle_lease { action: "coverage" }` — which code of a leased page ran, as the JS engine counted it.
3
+ *
4
+ * A caller outside the app cannot see which branches a drive reached. The JS engine can: each script's block
5
+ * ranges carry a count. This reads them from the lease's own Playwright page, the way
6
+ * `reticle_network_mock` reaches it, so it needs no change to the app.
7
+ *
8
+ * A take returns what ran since the previous take, and coverage keeps running across a reload.
9
+ * Start it on acquire (`jsCoverage: true`) to see the code that runs while the page loads; a start
10
+ * after the load misses it.
11
+ */
12
+ import { z } from 'zod';
13
+ import type { ScriptCoverage } from '../../portal/input/js-coverage.js';
14
+ import { type ToolDef } from './tool-kit.js';
15
+ export declare const LeaseCoverageMode: {
16
+ readonly START: "start";
17
+ readonly TAKE: "take";
18
+ };
19
+ /** Ranges one take returns when the caller sets no cap. */
20
+ export declare const DEFAULT_MAX_COVERAGE_RANGES = 20000;
21
+ interface Range {
22
+ startOffset: number;
23
+ endOffset: number;
24
+ count: number;
25
+ }
26
+ /** What a take returns: each kept script with its ranges, and what the cap and filter left out. */
27
+ export interface CoverageTake {
28
+ scripts: {
29
+ url: string;
30
+ ranges: Range[];
31
+ }[];
32
+ scriptCount: number;
33
+ rangeCount: number;
34
+ filteredOut?: number;
35
+ truncated?: {
36
+ scripts: number;
37
+ ranges: number;
38
+ maxRanges: number;
39
+ };
40
+ }
41
+ /**
42
+ * Flatten each script's functions into one range list, keep the scripts `urlContains` names, and
43
+ * stop at `maxRanges`. What the cap drops is counted in `truncated`, beside the value.
44
+ */
45
+ export declare function flattenCoverage(entries: readonly ScriptCoverage[], maxRanges: number, urlContains?: string): CoverageTake;
46
+ /** The acquire argument that starts coverage before the page's first script. */
47
+ export declare const LEASE_JS_COVERAGE_ARG: z.ZodOptional<z.ZodBoolean>;
48
+ export declare const LEASE_COVERAGE_TOOL: ToolDef;
49
+ export {};
@@ -0,0 +1,128 @@
1
+ /**
2
+ * `reticle_lease { action: "coverage" }` — which code of a leased page ran, as the JS engine counted it.
3
+ *
4
+ * A caller outside the app cannot see which branches a drive reached. The JS engine can: each script's block
5
+ * ranges carry a count. This reads them from the lease's own Playwright page, the way
6
+ * `reticle_network_mock` reaches it, so it needs no change to the app.
7
+ *
8
+ * A take returns what ran since the previous take, and coverage keeps running across a reload.
9
+ * Start it on acquire (`jsCoverage: true`) to see the code that runs while the page loads; a start
10
+ * after the load misses it.
11
+ */
12
+ import { z } from 'zod';
13
+ import { ReticleTool, asNumber, asString } from '@reticlehq/core';
14
+ import { sessionIdShape } from './tool-kit.js';
15
+ export const LeaseCoverageMode = { START: 'start', TAKE: 'take' };
16
+ /** Ranges one take returns when the caller sets no cap. */
17
+ export const DEFAULT_MAX_COVERAGE_RANGES = 20_000;
18
+ const MAX_COVERAGE_RANGES = 200_000;
19
+ const NOT_A_LEASE = 'reticle_lease { action: "coverage" } reads a leased tab only. Acquire one with reticle_lease { action: "acquire", url, jsCoverage: true }, then pass its sessionId.';
20
+ const CANNOT_MEASURE = 'This leased page cannot measure JS coverage. Coverage needs Chromium, and a page that is still open.';
21
+ const WAS_NOT_RUNNING = 'Coverage was not running, so this take holds nothing. It runs now: do the work, then take again. To see the code that runs while the page loads, acquire with jsCoverage: true.';
22
+ /**
23
+ * Flatten each script's functions into one range list, keep the scripts `urlContains` names, and
24
+ * stop at `maxRanges`. What the cap drops is counted in `truncated`, beside the value.
25
+ */
26
+ export function flattenCoverage(entries, maxRanges, urlContains) {
27
+ const kept = entries.filter((e) => urlContains === undefined || e.url.includes(urlContains));
28
+ const scripts = [];
29
+ let rangeCount = 0;
30
+ let droppedRanges = 0;
31
+ let droppedScripts = 0;
32
+ for (const entry of kept) {
33
+ const ranges = entry.functions.flatMap((f) => f.ranges);
34
+ const room = maxRanges - rangeCount;
35
+ if (room <= 0 && 0 < ranges.length) {
36
+ droppedScripts += 1;
37
+ droppedRanges += ranges.length;
38
+ continue;
39
+ }
40
+ const taken = ranges
41
+ .slice(0, Math.max(0, room))
42
+ .map(({ startOffset, endOffset, count }) => ({ startOffset, endOffset, count }));
43
+ droppedRanges += ranges.length - taken.length;
44
+ rangeCount += taken.length;
45
+ scripts.push({ url: entry.url, ranges: taken });
46
+ }
47
+ const filteredOut = entries.length - kept.length;
48
+ return {
49
+ scripts,
50
+ scriptCount: scripts.length,
51
+ rangeCount,
52
+ ...(0 < filteredOut ? { filteredOut } : {}),
53
+ ...(0 < droppedRanges
54
+ ? { truncated: { scripts: droppedScripts, ranges: droppedRanges, maxRanges } }
55
+ : {}),
56
+ };
57
+ }
58
+ /** The acquire argument that starts coverage before the page's first script. */
59
+ export const LEASE_JS_COVERAGE_ARG = z
60
+ .boolean()
61
+ .optional()
62
+ .describe('Start JS coverage before the page runs its first script, so reticle_lease { action: "coverage" } also counts the code that runs while the page loads.');
63
+ export const LEASE_COVERAGE_TOOL = {
64
+ name: ReticleTool.LEASE_COVERAGE,
65
+ description: 'JS coverage of a leased tab: which code ran. { mode: "start" } starts it; { mode: "take" } (the default) returns, for each script, its url and block ranges { startOffset, endOffset, count } since the previous take. A count of 0 is code that did not run. Acquire with jsCoverage: true to include the code that runs while the page loads. Use urlContains to keep only your own scripts; a take that hits maxRanges says what it dropped in `truncated`.',
66
+ example: { mode: 'take', urlContains: '/src/' },
67
+ inputSchema: {
68
+ mode: z
69
+ .enum([LeaseCoverageMode.START, LeaseCoverageMode.TAKE])
70
+ .optional()
71
+ .describe('"start" starts coverage; "take" (default) returns what ran since the previous take.'),
72
+ urlContains: z
73
+ .string()
74
+ .min(1)
75
+ .optional()
76
+ .describe('Keep only the scripts whose url contains this text, for example "/src/".'),
77
+ maxRanges: z
78
+ .number()
79
+ .int()
80
+ .min(1)
81
+ .max(MAX_COVERAGE_RANGES)
82
+ .optional()
83
+ .describe(`The most ranges one take returns. Default ${String(DEFAULT_MAX_COVERAGE_RANGES)}.`),
84
+ ...sessionIdShape,
85
+ },
86
+ outputSchema: {
87
+ running: z.boolean().describe('Whether coverage runs on the page after this call.'),
88
+ scripts: z
89
+ .array(z.object({
90
+ url: z.string(),
91
+ ranges: z.array(z.object({ startOffset: z.number(), endOffset: z.number(), count: z.number() })),
92
+ }))
93
+ .optional(),
94
+ scriptCount: z.number().optional(),
95
+ rangeCount: z.number().optional(),
96
+ filteredOut: z
97
+ .number()
98
+ .optional()
99
+ .describe('Scripts left out because their url did not contain urlContains.'),
100
+ truncated: z
101
+ .object({ scripts: z.number(), ranges: z.number(), maxRanges: z.number() })
102
+ .optional()
103
+ .describe('Present when the take hit maxRanges: the scripts and ranges it dropped. Absent means nothing was dropped.'),
104
+ hint: z.string().optional(),
105
+ },
106
+ handler: async (deps, args) => {
107
+ const session = deps.sessions.resolve(asString(args['sessionId']));
108
+ const coverage = deps.pool?.coverageOf(session.id);
109
+ if (coverage === undefined) {
110
+ throw new Error(NOT_A_LEASE);
111
+ }
112
+ if (LeaseCoverageMode.START === args['mode']) {
113
+ if (!(await coverage.start())) {
114
+ throw new Error(CANNOT_MEASURE);
115
+ }
116
+ return { running: true };
117
+ }
118
+ const entries = await coverage.take();
119
+ if (entries === undefined) {
120
+ if (!(await coverage.start())) {
121
+ throw new Error(CANNOT_MEASURE);
122
+ }
123
+ return { running: true, scripts: [], scriptCount: 0, rangeCount: 0, hint: WAS_NOT_RUNNING };
124
+ }
125
+ const maxRanges = asNumber(args['maxRanges']) ?? DEFAULT_MAX_COVERAGE_RANGES;
126
+ return { running: true, ...flattenCoverage(entries, maxRanges, asString(args['urlContains'])) };
127
+ },
128
+ };
@@ -17,7 +17,8 @@
17
17
  * So: rank by what the daemon actually knows, and fall back to the differential only when it knows
18
18
  * nothing. A cause we hold positive evidence against is not printed at all.
19
19
  */
20
- import type { LeaseReaderFailure } from './lease-readiness.js';
20
+ import { LeaseNotReadyReason } from '@reticlehq/core';
21
+ import { HeldLeaseReload, type LeaseReaderFailure } from './lease-readiness.js';
21
22
  /** What the daemon can say for certain at the moment a lease comes back unconnected. */
22
23
  export interface LeaseEvidence {
23
24
  /** A dial WAS made and turned away, with the reason. The only certain cause there is. */
@@ -72,3 +73,36 @@ export declare function leaseNotConnectedHint(url: string, port: number, evidenc
72
73
  export declare function leaseStillLoadingHint(url: string, waitedMs: number): string;
73
74
  /** The hint for a lease whose zero-install reader did not run (#1701). The lease is already released. */
74
75
  export declare function leaseReaderFailedHint(url: string, failure: LeaseReaderFailure): string;
76
+ /** What `reticle_lease` answers for a held lease that it loaded again and that did not connect. */
77
+ export interface KeptLeaseAnswer {
78
+ sessionId: string;
79
+ url: string;
80
+ ready: false;
81
+ notReadyReason: LeaseNotReadyReason;
82
+ reused: true;
83
+ expiresInMs: number;
84
+ leased: number;
85
+ queued: number;
86
+ hint: string;
87
+ }
88
+ /**
89
+ * The answer for a held lease that is kept although its page did not connect (#1924). The tab is
90
+ * kept, because its context holds the cookies: a new context starts signed out. `reload` says
91
+ * whether its page loaded again; `cause` is why it did not connect, or why it did not load.
92
+ */
93
+ export declare function keptLeaseAnswer(lease: {
94
+ sessionId: string;
95
+ origin: string;
96
+ url: string;
97
+ reload: HeldLeaseReload;
98
+ }, pool: {
99
+ leaseTtlMs(): number;
100
+ activeCount(): number;
101
+ queuedCount(): number;
102
+ }, cause: string): KeptLeaseAnswer;
103
+ /**
104
+ * Why a held lease's page did not load again, said from what the daemon can check: whether
105
+ * anything accepts a connection on the page's port. Only asked of a loopback URL, where the probe
106
+ * reaches the same server the browser does.
107
+ */
108
+ export declare function notLoadedCause(url: string, portAccepts: ((port: number) => Promise<boolean>) | undefined): Promise<string>;