@reticlehq/server 3.9.0 → 3.10.1

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 (351) 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 +43 -2
  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/init.mdx +2 -2
  322. package/docs/cli/license.mdx +9 -7
  323. package/docs/cli/restart.mdx +7 -5
  324. package/docs/cli/rollback.mdx +5 -5
  325. package/docs/cli/stop.mdx +3 -3
  326. package/docs/cli/update.mdx +2 -2
  327. package/docs/cli/verify.mdx +23 -1
  328. package/docs/cli.mdx +33 -23
  329. package/docs/deploy-checks.md +1 -1
  330. package/docs/desktop.mdx +1 -1
  331. package/docs/enterprise.md +2 -2
  332. package/docs/environment.mdx +1 -1
  333. package/docs/flows.md +11 -1
  334. package/docs/frameworks.mdx +9 -9
  335. package/docs/getting-started.md +1 -1
  336. package/docs/install-manual.mdx +1 -1
  337. package/docs/license-activation.mdx +3 -3
  338. package/docs/packages/tauri.mdx +1 -1
  339. package/docs/packages.mdx +1 -1
  340. package/docs/predicates.mdx +1 -1
  341. package/docs/quickstart.mdx +68 -49
  342. package/docs/skill-setup.md +1 -1
  343. package/docs/skill.md +14 -13
  344. package/docs/telemetry.md +2 -2
  345. package/docs/tools/navigate.mdx +1 -1
  346. package/docs/tools/verify.mdx +4 -1
  347. package/docs/troubleshooting.mdx +30 -1
  348. package/docs/usage.md +17 -14
  349. package/package.json +11 -14
  350. package/dist/features/exhaust/exhaust-run.d.ts +0 -33
  351. package/dist/features/exhaust/exhaust-run.js +0 -82
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Grade a finished run of an app from snapshots of its stored data. Pure.
3
+ *
4
+ * A snapshot is the state lines of `readStateSources`, and optionally a `url` line for the page the
5
+ * run ended on. Three snapshots go in: the reference (the app after the correct steps), again (the
6
+ * app after the same correct steps, from the same start), and the candidate (the app after the steps
7
+ * to grade). A line that differs between the two reference snapshots is noise and is ignored, and so
8
+ * is a change to a cell of a row that the two references wrote differently (see `grade-noise.ts`). Any
9
+ * other line that differs between the reference and the candidate is a difference, and the grade
10
+ * fails. A source that could not be read makes the grade unverified, never a pass.
11
+ *
12
+ * A row the candidate deleted and wrote again with a new id and the same values is not a difference
13
+ * (see `grade-renumber.ts`). The grade counts these rows.
14
+ *
15
+ * State lines are sorted rows, so the diff is a count of each line, not an ordered diff. That keeps
16
+ * the cost linear in the number of rows, and a source of many thousand rows is cheap.
17
+ */
18
+ import { z } from 'zod';
19
+ import { MSG_READ_FAILED } from '../compare/outside/state-sources.js';
20
+ import { noiseCells, withoutNoiseCells } from './grade-noise.js';
21
+ import { renumber } from './grade-renumber.js';
22
+ export const GradeVerdict = {
23
+ PASS: 'pass',
24
+ FAIL: 'fail',
25
+ /** The grade proves nothing: a source could not be read, or the answer could not be checked. */
26
+ UNVERIFIED: 'unverified',
27
+ };
28
+ export const SnapshotSchema = z.strictObject({
29
+ lines: z.array(z.string()),
30
+ });
31
+ /** The line that holds the page a run ended on. */
32
+ export const URL_PREFIX = 'url';
33
+ /**
34
+ * The page a run ended on, as one line: the origin and path without a final slash, and the query
35
+ * values sorted by name. The fragment is removed. Text that is not a URL is kept as it is.
36
+ */
37
+ export function urlLine(url) {
38
+ let parsed;
39
+ try {
40
+ parsed = new URL(url);
41
+ }
42
+ catch {
43
+ return `${URL_PREFIX} ${url.trim()}`;
44
+ }
45
+ parsed.searchParams.sort();
46
+ const path = parsed.pathname.replace(/\/+$/, '');
47
+ const query = parsed.searchParams.toString();
48
+ return `${URL_PREFIX} ${parsed.origin}${path}${'' === query ? '' : `?${query}`}`;
49
+ }
50
+ const counts = (lines) => {
51
+ const out = new Map();
52
+ for (const line of lines) {
53
+ out.set(line, (out.get(line) ?? 0) + 1);
54
+ }
55
+ return out;
56
+ };
57
+ /** The lines `a` has more times than `b`, once for each extra time. */
58
+ function surplus(a, b) {
59
+ const left = counts(b);
60
+ const out = [];
61
+ for (const line of a) {
62
+ const n = left.get(line) ?? 0;
63
+ if (n > 0) {
64
+ left.set(line, n - 1);
65
+ }
66
+ else {
67
+ out.push(line);
68
+ }
69
+ }
70
+ return out;
71
+ }
72
+ export function gradeState(reference, again, candidate) {
73
+ const unread = [reference, again, candidate].flatMap((s) => s.lines.filter((line) => line.startsWith(MSG_READ_FAILED)));
74
+ const referenceOnly = surplus(reference.lines, again.lines);
75
+ const againOnly = surplus(again.lines, reference.lines);
76
+ const noise = new Set([...referenceOnly, ...againOnly]);
77
+ const all = {
78
+ removed: surplus(reference.lines, candidate.lines),
79
+ added: surplus(candidate.lines, reference.lines),
80
+ };
81
+ const removedKept = all.removed.filter((line) => !noise.has(line));
82
+ const byCell = withoutNoiseCells(all.added.filter((line) => !noise.has(line)), noiseCells(referenceOnly, againOnly));
83
+ const { removed, added, renumbered } = renumber(removedKept, byCell.added, reference.lines, candidate.lines);
84
+ const ignored = all.removed.length + all.added.length - removedKept.length - byCell.added.length;
85
+ let verdict = GradeVerdict.PASS;
86
+ if (unread.length > 0) {
87
+ verdict = GradeVerdict.UNVERIFIED;
88
+ }
89
+ else if (removed.length > 0 || added.length > 0) {
90
+ verdict = GradeVerdict.FAIL;
91
+ }
92
+ return { verdict, removed, added, ignored, unread, renumbered };
93
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Write the state declaration for an app, so nobody writes one by hand. Pure.
3
+ *
4
+ * Two steps:
5
+ * 1. `tableSources`: one `command` source for each table of a database, from a list of table names
6
+ * and a command template with `{table}` in it. `skip` holds name prefixes. It removes the tables that hold no task data,
7
+ * such as a message queue that a background worker empties at its own time.
8
+ * 2. `quietSources`: keep the sources that read the same on two runs of the same correct steps.
9
+ * A source that changes between those two runs (a session table, a log) would make every
10
+ * candidate fail, because the candidate's own new rows are lines that neither reference has. Such
11
+ * a source is kept with a time pattern removed when that makes it read the same, else dropped.
12
+ * Each dropped source is reported with the reason.
13
+ *
14
+ * A source the reference does not change is kept too: a change to it by the candidate is a
15
+ * change to a record the task did not name.
16
+ */
17
+ import { type StateSource } from '../compare/outside/state-sources.js';
18
+ /** The word in a command template that the table name replaces. */
19
+ export declare const TABLE_SLOT = "{table}";
20
+ /** A date and time, with optional fractions of a second: the most common noise in a row. */
21
+ export declare const TIME_PATTERN = "\\d{4}-\\d\\d-\\d\\d[ T]\\d\\d:\\d\\d:\\d\\d(?:\\.\\d+)?";
22
+ export interface Skipped {
23
+ label: string;
24
+ reason: string;
25
+ }
26
+ export declare function tableSources(names: readonly string[], template: string, container?: string, skip?: readonly string[]): {
27
+ sources: StateSource[];
28
+ skipped: Skipped[];
29
+ };
30
+ export declare function quietSources(sources: readonly StateSource[], reference: readonly string[], again: readonly string[]): {
31
+ sources: StateSource[];
32
+ dropped: Skipped[];
33
+ };
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Write the state declaration for an app, so nobody writes one by hand. Pure.
3
+ *
4
+ * Two steps:
5
+ * 1. `tableSources`: one `command` source for each table of a database, from a list of table names
6
+ * and a command template with `{table}` in it. `skip` holds name prefixes. It removes the tables that hold no task data,
7
+ * such as a message queue that a background worker empties at its own time.
8
+ * 2. `quietSources`: keep the sources that read the same on two runs of the same correct steps.
9
+ * A source that changes between those two runs (a session table, a log) would make every
10
+ * candidate fail, because the candidate's own new rows are lines that neither reference has. Such
11
+ * a source is kept with a time pattern removed when that makes it read the same, else dropped.
12
+ * Each dropped source is reported with the reason.
13
+ *
14
+ * A source the reference does not change is kept too: a change to it by the candidate is a
15
+ * change to a record the task did not name.
16
+ */
17
+ import { MSG_READ_FAILED, MSG_ROWS_CUT, STATE_PREFIX, StateSourceKind, } from '../compare/outside/state-sources.js';
18
+ /** The word in a command template that the table name replaces. */
19
+ export const TABLE_SLOT = '{table}';
20
+ /** A table name that is safe in a shell command and in SQL without quotes. */
21
+ const TABLE_NAME = /^[A-Za-z0-9_$]+$/;
22
+ /** A date and time, with optional fractions of a second: the most common noise in a row. */
23
+ export const TIME_PATTERN = '\\d{4}-\\d\\d-\\d\\d[ T]\\d\\d:\\d\\d:\\d\\d(?:\\.\\d+)?';
24
+ export function tableSources(names, template, container, skip = []) {
25
+ const sources = [];
26
+ const skipped = [];
27
+ for (const raw of names) {
28
+ const name = raw.trim();
29
+ if ('' === name) {
30
+ continue;
31
+ }
32
+ if (skip.some((prefix) => name.startsWith(prefix))) {
33
+ skipped.push({ label: name, reason: 'the name starts with a --skip prefix' });
34
+ continue;
35
+ }
36
+ if (!TABLE_NAME.test(name)) {
37
+ skipped.push({ label: name, reason: 'the name has a character other than A-Z, 0-9, _ or $' });
38
+ continue;
39
+ }
40
+ sources.push({
41
+ kind: StateSourceKind.COMMAND,
42
+ label: name,
43
+ command: template.replaceAll(TABLE_SLOT, name),
44
+ ...(container === undefined ? {} : { container }),
45
+ });
46
+ }
47
+ return { sources, skipped };
48
+ }
49
+ /** The rows of one source in a snapshot, and the read failure when there is one. */
50
+ function rowsOf(lines, label) {
51
+ const row = `${STATE_PREFIX} ${label} `;
52
+ const cut = `${MSG_ROWS_CUT} ${label}: `;
53
+ const failed = lines.find((line) => line.startsWith(`${MSG_READ_FAILED} ${label}: `));
54
+ const rows = lines.filter((line) => line.startsWith(row) || line.startsWith(cut));
55
+ return failed === undefined ? { rows } : { rows, failed };
56
+ }
57
+ function sameRows(a, b) {
58
+ if (a.length !== b.length) {
59
+ return false;
60
+ }
61
+ const x = [...a].sort();
62
+ const y = [...b].sort();
63
+ return x.every((line, i) => line === y[i]);
64
+ }
65
+ /** The first row that only one of the two runs has, to show why a source was dropped. */
66
+ function firstDifference(a, b) {
67
+ const inA = new Set(a);
68
+ const inB = new Set(b);
69
+ return a.find((line) => !inB.has(line)) ?? b.find((line) => !inA.has(line)) ?? '';
70
+ }
71
+ export function quietSources(sources, reference, again) {
72
+ const kept = [];
73
+ const dropped = [];
74
+ const time = new RegExp(TIME_PATTERN, 'g');
75
+ for (const source of sources) {
76
+ const a = rowsOf(reference, source.label);
77
+ const b = rowsOf(again, source.label);
78
+ const failed = a.failed ?? b.failed;
79
+ if (failed !== undefined) {
80
+ dropped.push({ label: source.label, reason: failed });
81
+ continue;
82
+ }
83
+ if (sameRows(a.rows, b.rows)) {
84
+ kept.push(source);
85
+ continue;
86
+ }
87
+ const untimed = (rows) => rows.map((r) => r.replace(time, ''));
88
+ if (StateSourceKind.COMMAND === source.kind && sameRows(untimed(a.rows), untimed(b.rows))) {
89
+ kept.push({ ...source, ignore: [...(source.ignore ?? []), TIME_PATTERN] });
90
+ continue;
91
+ }
92
+ dropped.push({
93
+ label: source.label,
94
+ reason: `it changes between two runs of the same steps: ${firstDifference(a.rows, b.rows)}`,
95
+ });
96
+ }
97
+ return { sources: kept, dropped };
98
+ }
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { AppRuntime, ReticleCommand, VISUAL_NO_PROVIDER_RECOMMENDATION, VisualReason, } from '@reticlehq/core';
2
+ import { AppRuntime, ReticleCommand, VISUAL_NO_PROVIDER_RECOMMENDATION, VisualReason, realmOf, } from '@reticlehq/core';
3
3
  import { ReticleTool } from '@reticlehq/core';
4
4
  import { sessionIdShape } from '../../surface/tools/tool-kit.js';
5
5
  import { ratioSchema } from '../../surface/tools/args/numeric-bounds.js';
@@ -34,6 +34,38 @@ function screenshotProvider(deps) {
34
34
  ? p
35
35
  : undefined;
36
36
  }
37
+ /**
38
+ * Whether this call can photograph the session: a lease of this caller, a desktop shell, or a page
39
+ * that the driven browser has.
40
+ *
41
+ * A desktop shell counts before its capture is known to work. Only a CAPTURE command can tell, and
42
+ * a person who connected a desktop app most likely wants that window, not a lease beside it.
43
+ */
44
+ async function canCapture(deps, session) {
45
+ // A lease of this caller. Another agent's lease is never this caller's to photograph (#1226).
46
+ if (true === deps.pool?.heldBy(session.id, deps.attachId)) {
47
+ return true;
48
+ }
49
+ if (realmOf(session.runtime).isDesktopShell) {
50
+ return true;
51
+ }
52
+ // A driven browser photographs only a page it has. Having a driven browser is not enough.
53
+ const driven = screenshotProvider(deps);
54
+ if (driven === undefined) {
55
+ return false;
56
+ }
57
+ return driven.isAvailableFor(session.url);
58
+ }
59
+ /**
60
+ * The one session this call photographs, chosen once (#1959).
61
+ *
62
+ * capture() and captureRoot() each resolve the id again. With no id, each could choose another
63
+ * session, and capture() never tried a lease at all. After this, they all get the same, named id.
64
+ */
65
+ async function captureTarget(deps, args) {
66
+ const session = await deps.sessions.resolveServing(asString(args['sessionId']), (s) => canCapture(deps, s));
67
+ return session.id;
68
+ }
37
69
  /** The "you need a driven browser" envelope, shared by both visual tools. */
38
70
  const noProvider = {
39
71
  ok: false,
@@ -42,8 +74,9 @@ const noProvider = {
42
74
  };
43
75
  /** The answer when no pixels came back: the shell's own recommendation over the web one. */
44
76
  function unavailable(reason, recommendation) {
45
- if (recommendation !== undefined)
77
+ if (recommendation !== undefined) {
46
78
  return { ok: false, reason, recommendation };
79
+ }
47
80
  return reason === VisualReason.NO_PROVIDER ? noProvider : { ok: false, reason };
48
81
  }
49
82
  function asBox(value) {
@@ -52,10 +85,12 @@ function asBox(value) {
52
85
  const y = asNumber(b['y']);
53
86
  const w = asNumber(b['width']);
54
87
  const h = asNumber(b['height']);
55
- if (x === undefined || y === undefined || w === undefined || h === undefined)
88
+ if (x === undefined || y === undefined || w === undefined || h === undefined) {
56
89
  return undefined;
57
- if (w <= 0 || h <= 0)
90
+ }
91
+ if (w <= 0 || h <= 0) {
58
92
  return undefined;
93
+ }
59
94
  return { x, y, width: w, height: h };
60
95
  }
61
96
  /** Build capture options from args: explicit clip > ref (resolved via INSPECT) > fullPage. */
@@ -64,16 +99,18 @@ async function buildOpts(deps, sessionId, args) {
64
99
  if (clipArg !== undefined) {
65
100
  const c = asRecord(clipArg);
66
101
  const box = asBox({ box: c });
67
- if (box !== undefined)
102
+ if (box !== undefined) {
68
103
  return { clip: box };
104
+ }
69
105
  }
70
106
  const ref = asString(args['ref']);
71
107
  if (ref !== undefined) {
72
108
  const session = deps.sessions.resolve(sessionId);
73
109
  const res = await session.command(ReticleCommand.INSPECT, { ref });
74
110
  const box = res.ok ? asBox(res.result) : undefined;
75
- if (box !== undefined)
111
+ if (box !== undefined) {
76
112
  return { clip: box };
113
+ }
77
114
  }
78
115
  return true === args['fullPage'] ? { fullPage: true } : {};
79
116
  }
@@ -88,14 +125,16 @@ async function buildOpts(deps, sessionId, args) {
88
125
  export async function driveFrame(deps, sessionId, jpegQuality) {
89
126
  try {
90
127
  const leased = await deps.pool?.screenshotLease(sessionId, { jpegQuality });
91
- if (leased !== undefined)
128
+ if (leased !== undefined) {
92
129
  return leased;
130
+ }
93
131
  // The window `reticle drive` opened: neither a lease nor a desktop shell, but its page is ours.
94
132
  const url = deps.sessions.resolve(sessionId).url;
95
133
  if (true === (await deps.realInput?.isAvailableFor(url))) {
96
134
  const shot = await deps.realInput?.screenshot?.(url, { jpegQuality });
97
- if (shot !== undefined)
135
+ if (shot !== undefined) {
98
136
  return shot;
137
+ }
99
138
  }
100
139
  return (await desktopCapture(deps, sessionId, false)).png;
101
140
  }
@@ -153,8 +192,9 @@ async function capture(deps, sessionId, args) {
153
192
  // A driven browser renders the session's URL in a BROWSER — so the pixels are web even when the
154
193
  // session named is a desktop window. Scoping those under the desktop runtime would corrupt that
155
194
  // runtime's baseline with a picture of a different renderer.
156
- if (png !== undefined)
195
+ if (png !== undefined) {
157
196
  return { png, runtime: AppRuntime.WEB };
197
+ }
158
198
  }
159
199
  // The window's own backing store, via the Electron/Tauri adapter — the only route whose pixels
160
200
  // really are the desktop app.
@@ -167,8 +207,9 @@ async function capture(deps, sessionId, args) {
167
207
  }
168
208
  return { png: desktop.png, runtime: runtimeOf(deps, sessionId) };
169
209
  }
170
- if (desktop.reason !== undefined)
210
+ if (desktop.reason !== undefined) {
171
211
  return desktop;
212
+ }
172
213
  /*
173
214
  * A LEASED page is a real browser page, so the pixels were always there — the visual tools simply
174
215
  * had no route to them. Without this, `reticle_screenshot` answered "no provider" for every context
@@ -182,20 +223,23 @@ async function capture(deps, sessionId, args) {
182
223
  // shot of a lease saved the whole viewport and answered `saved: true`.
183
224
  await deps.pool?.screenshotLease(sessionId, await buildOpts(deps, sessionId, args));
184
225
  // A leased page is a real browser page, whatever the session is.
185
- if (leased !== undefined)
226
+ if (leased !== undefined) {
186
227
  return { png: leased, runtime: AppRuntime.WEB };
228
+ }
187
229
  return {
188
230
  reason: provider === undefined ? VisualReason.NO_PROVIDER : VisualReason.CAPTURE_FAILED,
189
231
  };
190
232
  }
191
233
  function rectsFrom(value) {
192
- if (!Array.isArray(value))
234
+ if (!Array.isArray(value)) {
193
235
  return undefined;
236
+ }
194
237
  const rects = [];
195
238
  for (const r of value) {
196
239
  const box = asBox({ box: r });
197
- if (box !== undefined)
240
+ if (box !== undefined) {
198
241
  rects.push(box);
242
+ }
199
243
  }
200
244
  return rects.length > 0 ? rects : undefined;
201
245
  }
@@ -237,10 +281,11 @@ export const VISUAL_TOOLS = [
237
281
  recommendation: z.string().optional(),
238
282
  },
239
283
  handler: async (deps, args) => {
240
- const sessionId = asString(args['sessionId']);
284
+ const sessionId = await captureTarget(deps, args);
241
285
  const { png, reason, recommendation, runtime } = await capture(deps, sessionId, args);
242
- if (png === undefined)
286
+ if (png === undefined) {
243
287
  return unavailable(reason, recommendation);
288
+ }
244
289
  const name = asString(args['name']) ?? 'default';
245
290
  // Beside the app that was captured. The tool RETURNS this path to the agent, so a wrong root
246
291
  // is not only a stray PNG — it is a path the agent is told to look at in a tree that has
@@ -290,22 +335,24 @@ export const VISUAL_TOOLS = [
290
335
  },
291
336
  handler: async (deps, args) => {
292
337
  const baseline = asString(args['baseline']) ?? '';
293
- const store = new VisualStore(deps.fs, captureRoot(deps, asString(args['sessionId'])));
338
+ const sessionId = await captureTarget(deps, args);
339
+ const store = new VisualStore(deps.fs, captureRoot(deps, sessionId));
294
340
  // Capture FIRST, then fetch the baseline for the runtime that produced these pixels. Reading it
295
341
  // by the session's runtime looked equivalent and is not: the route decides the renderer, so a
296
342
  // desktop session captured through a driven browser must be compared against the WEB baseline,
297
343
  // not the desktop one. Comparing across renderers is a confident wrong diff in one direction
298
344
  // and a silently overwritten baseline in the other.
299
- const sessionId = asString(args['sessionId']);
300
345
  const captured = await capture(deps, sessionId, args);
301
346
  const { png: current, runtime } = captured;
302
- if (current === undefined)
347
+ if (current === undefined) {
303
348
  return unavailable(captured.reason, captured.recommendation);
349
+ }
304
350
  // A baseline from another runtime is not missing by accident — it is a different rendering, and
305
351
  // "no baseline for this one" is the honest answer.
306
352
  const baselineBytes = await store.readBaseline(baseline, runtime);
307
- if (baselineBytes === undefined)
353
+ if (baselineBytes === undefined) {
308
354
  return { ok: false, reason: VisualReason.BASELINE_MISSING };
355
+ }
309
356
  const masks = rectsFrom(args['masks']);
310
357
  const threshold = asNumber(args['threshold']);
311
358
  const maxRatio = asNumber(args['maxRatio']);
package/dist/index.js CHANGED
@@ -54,6 +54,7 @@ import { AGENT_ALREADY_DRIVING_ELSEWHERE, shouldGreetWithLeaseNotice, } from './
54
54
  import { LeaseReaper } from './portal/pool/lease-reaper.js';
55
55
  import { findProjectConfig, readJournalEnabled, readProjectId, } from './command/cli/ports/resolve/cli-port.js';
56
56
  import { probeLaunchableChromium } from './launch-chromium.js';
57
+ import { portAcceptsConnection } from './portal/session/dev-server/dev-server-probe.js';
57
58
  import { attachJournal } from './wire-journal.js';
58
59
  import { reportOnboardingStep } from './telemetry/onboarding-funnel.js';
59
60
  import { readRetainPolicy } from './memory/journal/on-disk/retain-policy.js';
@@ -149,7 +150,7 @@ export async function start(options = {}) {
149
150
  // Then to the agent's own git worktree: every worktree declares the same projectId.
150
151
  bridge.sessions.setCheckoutScope(liveCheckoutSource());
151
152
  const baselines = new BaselineStore();
152
- const recordings = new RecordingStore(() => currentDrivenBy() !== undefined);
153
+ const recordings = new RecordingStore(() => currentDrivenBy() !== undefined, (session) => bridge.sessions.successorId(session));
153
154
  // drive precedence: driveUrl (launch+own a browser) → CDP (attach) → none.
154
155
  let pool;
155
156
  let leaseReaper;
@@ -188,7 +189,7 @@ export async function start(options = {}) {
188
189
  const project = new ProjectStore(fs, reticleRoot, { now });
189
190
  attachRouteLearning(bridge, projectStoreResolver(fs, project, reticleRoot, now));
190
191
  const annotations = new AnnotationStore();
191
- pool = createBrowserPool(options.headless ?? true, readerConnect(port, security.token), (leaseId, error) => pageErrorJournal.onPoolPageError(leaseId, error), (leaseId) => pageErrorJournal.onLeaseClosed(leaseId), bridge.nonces);
192
+ pool = createBrowserPool(options.headless ?? true, readerConnect(port, security.token), (leaseId, error) => pageErrorJournal.onPoolPageError(leaseId, error), (leaseId) => pageErrorJournal.onLeaseClosed(leaseId), bridge.nonces, (leaseId, dialog) => pageErrorJournal.onPoolDialog(leaseId, dialog));
192
193
  leaseReaper = new LeaseReaper(pool);
193
194
  leaseReaper.start();
194
195
  /*
@@ -227,6 +228,7 @@ export async function start(options = {}) {
227
228
  bridgePort: port,
228
229
  nonces: bridge.nonces,
229
230
  browserProbe: probeLaunchableChromium,
231
+ portAccepts: portAcceptsConnection,
230
232
  // The daemon's OWN project, so a tool can tell "this session is mine" from "this session
231
233
  // belongs to a sibling app under the same daemon". contract_save refuses on the second.
232
234
  ...(activeProjectId === undefined ? {} : { projectId: activeProjectId }),
@@ -365,7 +367,7 @@ export async function startDaemon(options = {}) {
365
367
  const flows = new FlowStore(fs, reticleRoot, { now }, { onWrote: () => syncNudge.run?.() });
366
368
  // Built here rather than inside `deps` below, so teardown can save what a drive recorded. Both
367
369
  // paths pass the same pair — `daemon-parity.test.ts` is what keeps them from drifting apart.
368
- const recordings = new RecordingStore(() => currentDrivenBy() !== undefined);
370
+ const recordings = new RecordingStore(() => currentDrivenBy() !== undefined, (session) => bridge.sessions.successorId(session));
369
371
  /*
370
372
  * Bound AFTER the sync daemon exists, read only when a run is actually written.
371
373
  *
@@ -388,7 +390,7 @@ export async function startDaemon(options = {}) {
388
390
  const project = new ProjectStore(fs, reticleRoot, { now });
389
391
  attachRouteLearning(bridge, projectStoreResolver(fs, project, reticleRoot, now));
390
392
  const annotations = new AnnotationStore();
391
- const pool = createBrowserPool(options.headless ?? true, readerConnect(port, security.token), (leaseId, error) => pageErrorJournal.onPoolPageError(leaseId, error), (leaseId) => pageErrorJournal.onLeaseClosed(leaseId), bridge.nonces);
393
+ const pool = createBrowserPool(options.headless ?? true, readerConnect(port, security.token), (leaseId, error) => pageErrorJournal.onPoolPageError(leaseId, error), (leaseId) => pageErrorJournal.onLeaseClosed(leaseId), bridge.nonces, (leaseId, dialog) => pageErrorJournal.onPoolDialog(leaseId, dialog));
392
394
  const leaseReaper = new LeaseReaper(pool);
393
395
  leaseReaper.start();
394
396
  /*
@@ -461,6 +463,7 @@ export async function startDaemon(options = {}) {
461
463
  bridgePort: port,
462
464
  nonces: bridge.nonces,
463
465
  browserProbe: probeLaunchableChromium,
466
+ portAccepts: portAcceptsConnection,
464
467
  ...agentWiring(bridge.sessions, leaseUrl),
465
468
  // A finished verification should not sit behind a one-minute timer — see ToolDeps.onRunPersisted.
466
469
  onRunPersisted: () => cloudSync.nudge(),
@@ -94,6 +94,8 @@ export function causalSummary(events, context = {}) {
94
94
  const signals = [];
95
95
  const fieldsChanged = [];
96
96
  let layoutShift;
97
+ // The shift inside this window, summed from each entry's own `shift` (#1784).
98
+ let windowShift;
97
99
  let longTasks = 0;
98
100
  let ignoredBookkeeping = 0;
99
101
  for (const event of events) {
@@ -166,7 +168,11 @@ export function causalSummary(events, context = {}) {
166
168
  pushUnique(fieldsChanged, data['field']);
167
169
  break;
168
170
  case EventType.PERF:
169
- if (data['metric'] === PerfMetric.CLS && 'number' === typeof data['value']) {
171
+ if (data['metric'] === PerfMetric.CLS && 'number' === typeof data['shift']) {
172
+ windowShift = (windowShift ?? 0) + data['shift'];
173
+ }
174
+ else if (data['metric'] === PerfMetric.CLS && 'number' === typeof data['value']) {
175
+ // An SDK older than #1784 sends only the running total: the most it allows is its max.
170
176
  layoutShift = Math.max(layoutShift ?? 0, data['value']);
171
177
  }
172
178
  else if (data['metric'] === PerfMetric.LONGTASK) {
@@ -182,6 +188,7 @@ export function causalSummary(events, context = {}) {
182
188
  // store, and shrinking it because the evidence was trimmed would make the transient window read as
183
189
  // narrower than it was — the one number here that must survive the trim intact.
184
190
  const settle = stateSettle(stateDiffs);
191
+ const shiftInWindow = windowShift ?? layoutShift;
185
192
  return {
186
193
  net: {
187
194
  total: netTotal,
@@ -204,7 +211,7 @@ export function causalSummary(events, context = {}) {
204
211
  ? {}
205
212
  : { fieldsChanged: cap(fieldsChanged, 'fieldsChanged', elided) }),
206
213
  ...(0 === Object.keys(elided).length ? {} : { elided }),
207
- ...(layoutShift === undefined ? {} : { layoutShift }),
214
+ ...(shiftInWindow === undefined ? {} : { layoutShift: shiftInWindow }),
208
215
  longTasks,
209
216
  ...(true === context.stateUnwatched ? { stateUnwatched: true } : {}),
210
217
  ...(true === context.truncated ? { truncated: true } : {}),
@@ -4,8 +4,20 @@
4
4
  * perspective a flow that drifted no longer behaves, so it must not read as green. The actionable
5
5
  * "why" is lifted from the replay's decision envelope (or its error) into failureReason.
6
6
  */
7
- import { ReplayStatus, RunFlowStatus, type FlowFile, type FlowReplayResult, type RunFlowResult } from '@reticlehq/core';
7
+ import { ReplayStatus, RunFlowStatus, type FlowFile, type FlowReplayResult, type RunFlowResult, type RunStepResult } from '@reticlehq/core';
8
8
  /** OK → PASS; UNVERIFIABLE → SKIPPED; DRIFT/ERROR → FAIL (a healed flow is produced by the heal path, not plain replay). */
9
9
  export declare function runFlowStatusOf(status: ReplayStatus): RunFlowStatus;
10
+ /** One step as synced: what it did, where, and whether it held. */
11
+ /**
12
+ * A run step with how far a sequence got. Kept out of the shared run schema on purpose: that schema
13
+ * ships in the page SDK, where every byte counts, and only the CLI score in this same process reads
14
+ * the field. A schema reader strips it, which loses nothing it needs.
15
+ */
16
+ export interface ScoredRunStep extends RunStepResult {
17
+ subSteps?: {
18
+ passed: number;
19
+ total: number;
20
+ };
21
+ }
10
22
  /** Map one replay (plus its measured duration) into a RunFlowResult for the artifact's flows[]. */
11
23
  export declare function mapReplayToFlowResult(replay: FlowReplayResult, durationMs: number, flow?: FlowFile): RunFlowResult;
@@ -15,7 +15,6 @@ export function runFlowStatusOf(status) {
15
15
  return RunFlowStatus.FAIL;
16
16
  }
17
17
  const clip = (text) => text.length > RUN_TEXT_MAX ? `${text.slice(0, RUN_TEXT_MAX - 1)}…` : text;
18
- /** One step as synced: what it did, where, and whether it held. */
19
18
  function stepResultOf(step) {
20
19
  return {
21
20
  step: step.step,
@@ -26,6 +25,7 @@ function stepResultOf(step) {
26
25
  ...(step.consequence === undefined ? {} : { consequence: clip(step.consequence) }),
27
26
  ...(step.error === undefined ? {} : { error: clip(step.error) }),
28
27
  ...(step.drift === undefined ? {} : { drift: step.drift.reasonKind }),
28
+ ...(step.subSteps === undefined ? {} : { subSteps: step.subSteps }),
29
29
  };
30
30
  }
31
31
  /**
@@ -10,12 +10,26 @@
10
10
  * same artifact byte-for-byte.
11
11
  */
12
12
  import { reportRunTelemetry } from '../../telemetry/run-telemetry.js';
13
- import { boundFlowName, RunFlowStatus, VerifyPhase, } from '@reticlehq/core';
13
+ import { boundFlowName, RunFlowStatus, VerifyPhase, ReplayStatus, } from '@reticlehq/core';
14
14
  import { buildVerificationRun, } from './artifact/build-verification-run.js';
15
15
  import { mapReplayToFlowResult } from './artifact/replay-mapping.js';
16
16
  import { buildRepairPackets } from './repair-prompt.js';
17
17
  import { withShotRun } from './default-run-id.js';
18
18
  import { buildRisks, classifyChangedFiles, } from './risk-classify.js';
19
+ /**
20
+ * A flow whose replay threw (its browser session was lost, the app stopped answering) proves
21
+ * nothing, and it must not take the other flows of the suite down with it. It is graded as a replay
22
+ * that could not be completed: skipped, never a pass, with the error as the reason.
23
+ */
24
+ function replayThatCouldNotRun(name, error) {
25
+ const message = error instanceof Error ? error.message : String(error);
26
+ return {
27
+ name,
28
+ status: ReplayStatus.UNVERIFIABLE,
29
+ steps: [],
30
+ unverifiable: { reason: `the replay stopped before it could be graded: ${message}` },
31
+ };
32
+ }
19
33
  export class ReticleRunner {
20
34
  #port;
21
35
  constructor(port) {
@@ -58,7 +72,7 @@ export class ReticleRunner {
58
72
  name: boundFlowName(name),
59
73
  at: start,
60
74
  });
61
- const replay = await withShotRun(runId, () => this.#port.replayFlow(name));
75
+ const replay = await withShotRun(runId, () => this.#port.replayFlow(name)).catch((error) => replayThatCouldNotRun(name, error));
62
76
  replays.push(replay);
63
77
  const mapped = mapReplayToFlowResult(replay, this.#port.now() - start);
64
78
  flows.push(mapped);
@@ -12,4 +12,6 @@ export declare function createRunnerPort(deps: ToolDeps, sessionId?: string,
12
12
  /** `reticle verify --confirm-dangerous`: this run acknowledges the steps held as destructive. */
13
13
  opts?: {
14
14
  confirmDangerous?: boolean;
15
+ requireStartControl?: boolean;
16
+ sweep?: boolean;
15
17
  }): RunnerPort;
@@ -4,7 +4,10 @@
4
4
  * and a uuid generator. The ReticleRunner core (tested with a fake port) holds all the orchestration and
5
5
  * verdict logic, so this layer carries no decisions.
6
6
  */
7
+ import { ReplayStatus } from '@reticlehq/core';
7
8
  import { replayAndLearn } from '../../language/flows/flow-learning.js';
9
+ import { sessionUrlOf } from '../../language/flows/flow-lease.js';
10
+ import { flowStartState } from '../../surface/tools/session-ports.js';
8
11
  import { flowsForSession } from '../../language/flows/flow-store-for-session.js';
9
12
  import { safeSessionTarget } from '../../memory/project/session-root.js';
10
13
  import { defaultRunId } from './default-run-id.js';
@@ -13,17 +16,32 @@ export { defaultRunId } from './default-run-id.js';
13
16
  export function createRunnerPort(deps, sessionId,
14
17
  /** `reticle verify --confirm-dangerous`: this run acknowledges the steps held as destructive. */
15
18
  opts = {}) {
19
+ // Each flow starts from the state the run started in, so one that signs out cannot fail the rest.
20
+ const startState = flowStartState(deps.realInput, () => sessionUrlOf(deps, sessionId));
16
21
  return {
17
22
  // Resolved as replay resolves it, so every listed name is one replay can load.
18
23
  listFlows: () => {
19
24
  const target = safeSessionTarget(deps, sessionId);
20
25
  return flowsForSession(deps, target).flows.list(target.projectId);
21
26
  },
22
- replayFlow: (name) => replayAndLearn(deps, {
23
- flowName: name,
24
- ...(sessionId === undefined ? {} : { sessionId }),
25
- ...(true === opts.confirmDangerous ? { confirmDangerous: true } : {}),
26
- }),
27
+ replayFlow: async (name) => {
28
+ const cannotStart = await startState();
29
+ if (cannotStart !== undefined) {
30
+ return {
31
+ name,
32
+ status: ReplayStatus.UNVERIFIABLE,
33
+ steps: [],
34
+ unverifiable: { reason: cannotStart },
35
+ };
36
+ }
37
+ return await replayAndLearn(deps, {
38
+ flowName: name,
39
+ ...(sessionId === undefined ? {} : { sessionId }),
40
+ ...(true === opts.confirmDangerous ? { confirmDangerous: true } : {}),
41
+ ...(true === opts.requireStartControl ? { requireStartControl: true } : {}),
42
+ ...(true === opts.sweep ? { sweep: true } : {}),
43
+ });
44
+ },
27
45
  now: () => deps.now(),
28
46
  newRunId: defaultRunId,
29
47
  };