@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
package/SKILL.md CHANGED
@@ -2,30 +2,31 @@
2
2
 
3
3
  Reticle is the proof layer for AI coding agents. A dev-only SDK inside the user's running app gives you `reticle_*` MCP tools that read the DOM, network, routing, console and framework state. You act on the real app and get a verdict: `yes`, `no`, or `unknown` (could not tell), with evidence and the `file:line` to open. No screenshots: it drives the tab the user already has open.
4
4
 
5
- **Your first goal: one real verdict in the user's app.** Do the three steps in order. Do not stop to explain Reticle first.
5
+ **Your first goal: one real verdict in the user's app.** Do the steps in order. Do not stop to explain Reticle first.
6
6
 
7
- ## 1. Install (once per machine)
7
+ ## 1. Check the app, with nothing installed
8
8
 
9
- **`reticle_*` tools visible?** Go to step 2.
9
+ **`reticle_*` tools visible?** Call `reticle_session { action: "list" }`. It changes no file. On a wired project, go to step 3. On a project that is not wired, its `next_step` names the zero-install lease and asks the consent question for `init`.
10
10
 
11
- **Not visible?** Hand the user one line for a terminal, outside this client, and stop:
11
+ **No tools, or an app with no `.reticle.json`?** Run this in the app's folder. It changes no file:
12
12
 
13
13
  ```bash
14
- curl -fsSL https://raw.githubusercontent.com/reticlehq/reticle/main/install/install.sh | sh
15
- irm https://raw.githubusercontent.com/reticlehq/reticle/main/install/install.ps1 | iex # Windows
14
+ npx @reticlehq/server@latest
16
15
  ```
17
16
 
18
- It registers Reticle with every coding agent it finds and prints the next steps: restart the agent (a client loads MCP servers only at start, so a prompt cannot replace this command), then paste the prompt it printed. Do not register the server yourself and work around your client not having reloaded. If your client did register mid-session, `npx @reticlehq/server init --relaunch` prints the command that resumes this conversation with the tools.
17
+ It starts the dev server if the app does not run, opens the app in a Reticle browser and checks it: the page loads, no console error, no failed request, and each same-origin link opens. It prints `verified: yes` or `verified: no` with each fault. Show the user that verdict, then offer `init`.
19
18
 
20
19
  ## 2. Wire this app (once per project)
21
20
 
22
- In the project, call `reticle_session { action: "list" }`. It changes no file. On a project that is not wired, its `next_step` asks the consent question for `init`. When an app is running, it also names the zero-install lease, which checks that app now with nothing installed. `init` changes files, so run it only when the user agrees. A request to set up Reticle is that agreement. Run it in the app's folder:
21
+ `init` changes files, so run it only when the user agrees. A request to set up Reticle is that agreement. Run it in the app's folder:
23
22
 
24
23
  ```bash
25
24
  RETICLE_INSTALL_SOURCE=skill_file npx @reticlehq/server@latest init
26
25
  ```
27
26
 
28
- A dev server that was running when `init` ran must be restarted once. If `init` cannot finish alone, read SETUP below.
27
+ It registers Reticle with every coding agent it finds. A dev server that was running when `init` ran must be restarted once. If `init` cannot finish alone, read SETUP below.
28
+
29
+ **Still no `reticle_*` tools?** A client loads MCP servers only at start, so a prompt cannot add them. `npx @reticlehq/server init --relaunch` prints the command that resumes this conversation with the tools. Else ask the user to restart the agent. Do not register the server yourself and work around your client not having reloaded.
29
30
 
30
31
  ## 3. Prove one flow
31
32
 
@@ -76,7 +77,7 @@ The advertised surface is deliberately small (`default` 10, `all` 30). `RETICLE_
76
77
  | `reticle_verify` | replay, coverage, Harness | `action`: `change` `flows` `affected` `coverage` `crawl` `explore` `mutate` `heal` |
77
78
  | `reticle_tools` / `reticle_run` | everything else (below) | `names`; `tool`, `args` |
78
79
 
79
- Actions: `click` `dblclick` `hover` `focus` `blur` `fill` `type` `clear` `select` `check` `uncheck` `submit` `press` `upload` `scrollIntoView` `scroll` `drag` `tap` `zoom`. `args`: `{ value }` for fill/select, `{ text: "Enter" }` for press, `{ toRef }` for drag, `{ path }` for upload, `{ native: true }` for a trusted click. A control whose label reads as destructive is refused until you pass `args.confirmDangerous: true`, and only do that when the user asked for it.
80
+ Actions: `click` `dblclick` `hover` `focus` `blur` `fill` `type` `clear` `select` `check` `uncheck` `submit` `press` `upload` `scrollIntoView` `scroll` `drag` `tap` `zoom` `insertText`. `args`: `{ value }` for fill/select, `{ text: "Enter" }` for press, `{ toRef }` for drag, `{ path }` for upload, `{ native: true }` for a trusted click. `insertText` takes `{ text }` and types into a contenteditable/Tiptap/ProseMirror editor, where `fill` and `type` are refused. A control whose label reads as destructive is refused until you pass `args.confirmDangerous: true`, and only do that when the user asked for it.
80
81
 
81
82
  Beyond the advertised set, one hop each:
82
83
 
@@ -94,7 +95,7 @@ Every `until`, `predicate` and `expect` is `{ kind, ...fields }`; unknown fields
94
95
 
95
96
  | kind | fields |
96
97
  | --- | --- |
97
- | `element` | `query: { testid \| role+name \| text \| label \| placeholder \| alt \| component, scope? }`, `state?`: `visible` `hidden` `enabled` `disabled` `checked` `expanded` `pressed` `focused` `present` `inViewport`, `absent?` |
98
+ | `element` | `query: { testid \| role+name \| text \| label \| placeholder \| alt \| component, scope? }`, `state?`: `visible` `hidden` `enabled` `disabled` `checked` `expanded` `pressed` `focused` `present` `inViewport` `receivesPointer`, `absent?` |
98
99
  | `text` | `contains`, `scope?`, `visible?`, `absent?`, `ignoreCase?`, `satisfies?` |
99
100
  | `net` | `method`, `urlContains`, `status`, `ok`, `count` (exact; omit = ≥1; 0 = never), `bodyContains`, `bodyMatches`, `requestBodyMatches`, `repeatable` |
100
101
  | `route` | `pathname` or `contains` |
@@ -104,7 +105,7 @@ Every `until`, `predicate` and `expect` is `{ kind, ...fields }`; unknown fields
104
105
  | `settled` / `animation` / `compare` | `quietMs`, `channel?` / `name`, `completed` / `left`, `right`, `as` |
105
106
  | `allOf` / `anyOf` / `not` | `predicates: [...]` / `predicates: [...]` / `predicate` |
106
107
 
107
- `satisfies` asserts a property when the exact value varies: `nonEmpty`, `oneOf`, `withinTolerance`, `matchesPattern`, `type`, `changed`, `unchanged`, `increased`, `decreased`. Composites hold only predicates: `absent` belongs inside a child, and an `element` takes a `query`, never a `ref`. A bare `route` or `state` is already true before the action and proves nothing; pair it with what the action causes.
108
+ `satisfies` asserts a property when the exact value varies: `nonEmpty`, `oneOf`, `withinTolerance`, `matchesPattern`, `type`, `changed`, `unchanged`, `increased`, `decreased`. Composites hold only predicates: `absent` belongs inside a child, and an `element` takes a `query`, never a `ref`. A bare `route` or `state` is already true before the action and proves nothing; pair it with what the action causes. `absent` means not in the DOM; to say a thing is not shown, add `visible: true`. Assert what the requirement names, not a label you saw: another correct build can name it differently.
108
109
 
109
110
  ## Feedback is first-class
110
111
 
@@ -101,6 +101,8 @@ async function replaySuite(caller, options) {
101
101
  ...(options.select === undefined ? {} : { labels: options.select }),
102
102
  ...(options.sessionId === undefined ? {} : { sessionId: options.sessionId }),
103
103
  ...(true === options.confirmDangerous ? { confirmDangerous: true } : {}),
104
+ // The CLI waits as long as its own timeout, not the MCP default of 240 s, which is there for clients that give up at about 300 s.
105
+ budgetMs: SUITE_TIMEOUT_MS,
104
106
  // The running daemon's environment is not this command's: the secrets a CI step set for
105
107
  // `reticle verify` go with the call, for this call only.
106
108
  ...secretsArg(process.env),
@@ -1,3 +1,5 @@
1
+ import { type ToolCaller } from './daemon-link/daemon-client.js';
2
+ export { acquireLease, connectOverSse, endpointFor, refusalText, releaseLease, verdictOf, type ToolCaller, } from './daemon-link/daemon-client.js';
1
3
  /**
2
4
  * A verdict from the CLI, against the daemon that is already running.
3
5
  *
@@ -83,54 +85,12 @@ root?: string): Promise<{
83
85
  } | {
84
86
  failed: string[];
85
87
  }>;
86
- /**
87
- * Open `url` in a Reticle-leased browser unconditionally: `{ leased }` names the lease, which the
88
- * caller hands to `releaseLease`; `{ failed }` is the lease's own refusal, as lines to print.
89
- * `init`'s connect proof on a machine with no system browser takes this path directly.
90
- */
91
- export declare function acquireLease(caller: ToolCaller, url: string,
92
- /** Open it in a window somebody can watch, whatever the daemon was started as. */
93
- headed?: boolean,
94
- /** The `.reticle` the lease's runs belong in: the caller's project, not wherever the page resolves. */
95
- root?: string): Promise<{
96
- leased: string;
97
- zeroInstall: boolean;
98
- } | {
99
- failed: string[];
100
- }>;
101
- /** Hand back a lease `leaseIfNoTab` opened. Best-effort: an expired lease is already gone. */
102
- export declare function releaseLease(caller: ToolCaller, sessionId: string): Promise<void>;
103
88
  /**
104
89
  * The longest one `reticle_assert` call waits: its `timeout_ms` cap, one MCP request less slack.
105
90
  * Named here rather than imported, because this directory does not reach the tool layer; pinned
106
91
  * equal to `MAX_BLOCKING_WAIT_MS` by adhoc-verdict.test.ts so the two cannot drift apart.
107
92
  */
108
93
  export declare const ASSERT_MAX_WAIT_MS = 55000;
109
- /** The narrow slice of an MCP client this needs — one call, then close. */
110
- export interface ToolCaller {
111
- call(name: string, args: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
112
- close(): Promise<void>;
113
- }
114
- export declare function endpointFor(port: number, token: string | undefined): URL;
115
- export declare function connectOverSse(endpoint: URL): Promise<ToolCaller>;
116
- /**
117
- * The report object an MCP tool result carries, or undefined when the shape is not one.
118
- *
119
- * `field` is the key that makes a payload the report this caller wants: `verified` for a one-shot
120
- * verdict, `status` for a suite. It is a parameter rather than a fixed name because the suite path
121
- * reproduced this function's own incident the moment it reused it — the headline printed
122
- * `unverifiable` while the JSON under it said `"status":"fail"` with 74 failing flows, because a
123
- * suite report has no `verified` key and the text reader would only accept one that did.
124
- */
125
- export declare function verdictOf(result: unknown, field?: string): Record<string, unknown> | undefined;
126
- /**
127
- * The tool's own words when it refused, rather than the envelope carrying them.
128
- *
129
- * A refusal from these tools is the most useful text they produce — it names the cause and the next
130
- * action — and printing the MCP result object around it buries that in JSON escaping. The reader is
131
- * at a terminal; give them the sentence.
132
- */
133
- export declare function refusalText(result: unknown): string | undefined;
134
94
  /**
135
95
  * Navigate (optionally) and assert, against the daemon already on this port.
136
96
  *
@@ -146,4 +106,3 @@ export declare function runAdhocVerdict(options: AdhocVerdictOptions): Promise<A
146
106
  * caller that wants the finished report asks again by `runId` rather than starting a second drive.
147
107
  */
148
108
  export declare function exploreToEnd(caller: ToolCaller, args: Record<string, unknown>, budgetMs: number, now?: () => number): Promise<unknown>;
149
- export {};
@@ -1,30 +1,8 @@
1
- import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
- import { CLI_CLIENT_NAME } from '../../judgement/runs/verification-sync.js';
3
- import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
4
- import { DEFAULT_ASSERT_TIMEOUT_MS, LOOPBACK_HOST, MCP_SSE_PATH } from '@reticlehq/core';
5
- import { TOKEN_QUERY_PARAM } from '../../portal/bridge/token-auth.js';
6
- import { ReticleTool, asString as asText } from '@reticlehq/core';
1
+ import { DEFAULT_ASSERT_TIMEOUT_MS, ReticleTool, asString as asText } from '@reticlehq/core';
7
2
  import { decideOpen } from './launch/cli-launch.js';
8
- import { LEASE_KEPT_SENTENCE, LEASE_RELEASED_SENTENCE } from '../../surface/tools/lease-hint.js';
9
- /** The lease tool is reached through `reticle_run`, the route that works on every tool profile. */
10
- const LeaseAction = { ACQUIRE: 'acquire', RELEASE: 'release' };
11
- /**
12
- * Generous on purpose: on a machine with no Chrome, Edge or Playwright Chromium the FIRST lease
13
- * downloads Chromium before it can open anything (see launch-chromium), which outlives the MCP
14
- * client's 60s default and would report a timeout for a lease that was about to succeed.
15
- */
16
- const LEASE_TIMEOUT_MS = 300_000;
17
- const leaseFailedLine = (url) => `could not open ${url} in a Reticle browser, so nothing was checked:`;
18
- /**
19
- * Why a lease's tab is not ready: its `notReadyReason`, then its `hint`, when it gave them. The CLI
20
- * released the lease, so the hint's kept-lease sentence becomes the released-lease sentence.
21
- */
22
- function notReadyLine(report) {
23
- const reason = asText(report?.['notReadyReason']);
24
- const hint = asText(report?.['hint'])?.replace(LEASE_KEPT_SENTENCE, LEASE_RELEASED_SENTENCE);
25
- const head = reason === undefined ? 'the tab did not connect.' : `the tab did not connect (${reason}).`;
26
- return hint === undefined ? head : `${head} ${hint}`;
27
- }
3
+ import { acquireLease, connectOverSse, endpointFor, refusalText, releaseLease, verdictOf, } from './daemon-link/daemon-client.js';
4
+ // Re-exported so every existing importer of the daemon link through this file is unaffected.
5
+ export { acquireLease, connectOverSse, endpointFor, refusalText, releaseLease, verdictOf, } from './daemon-link/daemon-client.js';
28
6
  /**
29
7
  * Open `url` in a Reticle-leased browser when the daemon has no tab on its origin.
30
8
  *
@@ -75,64 +53,6 @@ function sameDocument(a, b) {
75
53
  return false;
76
54
  }
77
55
  }
78
- /**
79
- * Open `url` in a Reticle-leased browser unconditionally: `{ leased }` names the lease, which the
80
- * caller hands to `releaseLease`; `{ failed }` is the lease's own refusal, as lines to print.
81
- * `init`'s connect proof on a machine with no system browser takes this path directly.
82
- */
83
- export async function acquireLease(caller, url,
84
- /** Open it in a window somebody can watch, whatever the daemon was started as. */
85
- headed,
86
- /** The `.reticle` the lease's runs belong in: the caller's project, not wherever the page resolves. */
87
- root) {
88
- const ask = (withRoot) => caller.call(ReticleTool.RUN, {
89
- tool: ReticleTool.LEASE,
90
- args: {
91
- action: LeaseAction.ACQUIRE,
92
- url,
93
- ...(true === headed ? { headed } : {}),
94
- ...(withRoot && root !== undefined ? { root } : {}),
95
- },
96
- }, LEASE_TIMEOUT_MS);
97
- let acquired = await ask(true);
98
- // A daemon from an older release does not know `root` and refuses the whole call, so the page
99
- // was never opened. Ask once more without it: the lease works, only its runs land where that
100
- // daemon puts them.
101
- if (root !== undefined && REFUSES_ROOT.test(refusalOf(acquired) ?? '')) {
102
- acquired = await ask(false);
103
- }
104
- const report = verdictOf(acquired, 'sessionId');
105
- const sessionId = report?.['sessionId'];
106
- // `ready: false` still carries a sessionId, but no session is registered under it. Driving it
107
- // answered "names no session" and hid the cause the lease gave. Only an explicit false counts.
108
- if ('string' === typeof sessionId && false === report?.['ready']) {
109
- await releaseLease(caller, sessionId);
110
- return { failed: [leaseFailedLine(url), notReadyLine(report)] };
111
- }
112
- // `zeroInstall`: the page never dialled in, so the lease supplied Reticle's own reader.
113
- if ('string' === typeof sessionId) {
114
- return { leased: sessionId, zeroInstall: true === report?.['zeroInstall'] };
115
- }
116
- return {
117
- failed: [leaseFailedLine(url), refusalOf(acquired) ?? JSON.stringify(acquired)],
118
- };
119
- }
120
- /** The lease refusal an older daemon answers when it does not know the `root` argument. */
121
- const REFUSES_ROOT = /unknown parameter[^\n]*\broot\b/i;
122
- /** What the lease said no with: the `error` of its payload, else the refusal text. */
123
- function refusalOf(acquired) {
124
- const why = verdictOf(acquired, 'error')?.['error'];
125
- return 'string' === typeof why ? why : refusalText(acquired);
126
- }
127
- /** Hand back a lease `leaseIfNoTab` opened. Best-effort: an expired lease is already gone. */
128
- export async function releaseLease(caller, sessionId) {
129
- await caller
130
- .call(ReticleTool.RUN, {
131
- tool: ReticleTool.LEASE,
132
- args: { action: LeaseAction.RELEASE, sessionId },
133
- })
134
- .catch(() => undefined);
135
- }
136
56
  /** How long `--expect` waits without a `--timeout`: the wait form's own default. */
137
57
  const DEFAULT_EXPECT_WAIT_MS = DEFAULT_ASSERT_TIMEOUT_MS;
138
58
  /** Room for the daemon to answer after the wait it was given has run out. */
@@ -152,31 +72,55 @@ function runRefusal(result) {
152
72
  const said = verdictOf(result, 'error')?.['error'];
153
73
  return refusalText(result) ?? ('string' === typeof said ? said : undefined);
154
74
  }
75
+ /** The `reticle_assert` action that waits for a predicate within a budget. */
76
+ const ASSERT_WAIT_ACTION = 'wait';
77
+ /**
78
+ * The refusal of a daemon whose `reticle_assert` takes no `action`: every surface but the merged
79
+ * one, where `reticle_wait_for` is still a tool of its own.
80
+ */
81
+ const REFUSES_ACTION = /unknown parameter[^\n]*\baction\b/i;
82
+ /**
83
+ * One wait for the predicate, through `reticle_assert { action: "wait" }` (#2144).
84
+ *
85
+ * It went through `reticle_run { tool: "reticle_wait_for" }`. That name was merged into
86
+ * `reticle_assert`, and a 3.9.0 daemon answered it with "reticle_wait_for was merged into
87
+ * reticle_assert", so every `--expect` with a long `--timeout` printed `verified: unknown`. A daemon
88
+ * whose `reticle_assert` takes no action still has `reticle_wait_for` as a tool, so that is the
89
+ * one place the name is still used.
90
+ */
91
+ async function waitOnce(caller, args, timeoutMs) {
92
+ const waited = await caller.call(ReticleTool.ASSERT, { action: ASSERT_WAIT_ACTION, ...args }, timeoutMs);
93
+ const refused = runRefusal(waited);
94
+ if (refused === undefined || !REFUSES_ACTION.test(refused)) {
95
+ return waited;
96
+ }
97
+ return caller.call(ReticleTool.RUN, { tool: ReticleTool.WAIT_FOR, args }, timeoutMs);
98
+ }
155
99
  /**
156
100
  * Wait for the expectation, then grade it with ONE `reticle_assert` (#1533).
157
101
  *
158
- * The grade is `reticle_assert`'s, never the wait's: it journals, captures the assertion, and
159
- * carries `verifiedReason`, contradictions and coverage, which `reticle_wait_for` does not.
160
- * `reticle_assert` already waits when `timeout_ms > 0`, up to one request. A longer budget spends
161
- * the part one assert cannot hold in `reticle_wait_for` first, through `reticle_run` (the route
162
- * every tool profile has), following `resume_ms`, and then asserts. No `since` is passed by
163
- * either: both default to the same last-act cursor, and nothing here acts in between.
102
+ * The grade is the final `reticle_assert`'s: it journals, captures the assertion, and carries
103
+ * `verifiedReason`, contradictions and coverage. `reticle_assert` already waits when
104
+ * `timeout_ms > 0`, up to one request. A longer budget spends the part one assert cannot hold in
105
+ * `reticle_assert { action: "wait" }` first, following `resume_ms`, and then asserts. No `since` is
106
+ * passed by either: both default to the same last-act cursor, and nothing here acts in between.
164
107
  */
165
108
  async function waitForExpectation(caller, predicate, pin, timeoutMs) {
166
109
  const budget = Math.max(0, timeoutMs ?? DEFAULT_EXPECT_WAIT_MS);
167
110
  let settle = budget - ASSERT_MAX_WAIT_MS;
168
111
  while (0 < settle) {
169
- const waited = await caller.call(ReticleTool.RUN, { tool: ReticleTool.WAIT_FOR, args: { predicate, timeout_ms: settle, ...pin } },
170
- // The daemon blocks no longer than one request allows and answers resume_ms past that, so
171
- // the budget plus slack always outlasts the call.
172
- settle + WAIT_CALL_SLACK_MS);
112
+ // The daemon blocks no longer than one request allows and answers resume_ms past that, so the
113
+ // budget plus slack always outlasts the call.
114
+ const waited = await waitOnce(caller, { predicate, timeout_ms: settle, ...pin }, settle + WAIT_CALL_SLACK_MS);
173
115
  const refused = runRefusal(waited);
174
- if (refused !== undefined)
116
+ if (refused !== undefined) {
175
117
  return { refused };
176
- // By `pass`, which every wait answers with: a "not yet" carries no `verified`.
177
- const report = verdictOf(waited, 'pass');
118
+ }
119
+ // In the shape `reticle_assert` answers: a final answer carries `verified`, and only a wait
120
+ // that the per-call cap stopped carries `resume_ms` without one.
121
+ const report = verdictOf(waited, 'resume_ms');
178
122
  const resume = report?.['resume_ms'];
179
- if (true === report?.['pass'] ||
123
+ if (report?.['verified'] !== undefined ||
180
124
  'number' !== typeof resume ||
181
125
  resume <= 0 ||
182
126
  resume >= settle) {
@@ -191,91 +135,6 @@ async function waitForExpectation(caller, predicate, pin, timeoutMs) {
191
135
  }
192
136
  /** `verified` is the only field that decides the exit code. Everything else is for the reader. */
193
137
  const PROVED = 'yes';
194
- export function endpointFor(port, token) {
195
- const url = new URL(`http://${LOOPBACK_HOST}:${String(port)}${MCP_SSE_PATH}`);
196
- if (token !== undefined && token.length > 0)
197
- url.searchParams.set(TOKEN_QUERY_PARAM, token);
198
- return url;
199
- }
200
- export async function connectOverSse(endpoint) {
201
- const client = new Client({ name: CLI_CLIENT_NAME, version: '1' }, { capabilities: {} });
202
- await client.connect(new SSEClientTransport(endpoint));
203
- return {
204
- call: async (name, args, timeoutMs) => await client.callTool({ name, arguments: args }, undefined, timeoutMs === undefined ? undefined : { timeout: timeoutMs }),
205
- close: async () => {
206
- await client.close();
207
- },
208
- };
209
- }
210
- /**
211
- * The report object an MCP tool result carries, or undefined when the shape is not one.
212
- *
213
- * `field` is the key that makes a payload the report this caller wants: `verified` for a one-shot
214
- * verdict, `status` for a suite. It is a parameter rather than a fixed name because the suite path
215
- * reproduced this function's own incident the moment it reused it — the headline printed
216
- * `unverifiable` while the JSON under it said `"status":"fail"` with 74 failing flows, because a
217
- * suite report has no `verified` key and the text reader would only accept one that did.
218
- */
219
- export function verdictOf(result, field = 'verified') {
220
- const structured = result?.structuredContent;
221
- if ('object' === typeof structured && null !== structured) {
222
- return structured;
223
- }
224
- // A daemon that answers with the verdict as TEXT left this undefined, so the headline read
225
- // `verified: unknown` while the JSON printed underneath it said `"verified":"no"` — one response
226
- // giving two answers to the same question. The text is the same object, so read it.
227
- return verdictFromText(result, field);
228
- }
229
- /** The report object inside an MCP text part, when the result carried it there instead. */
230
- function verdictFromText(result, field) {
231
- const content = result?.content;
232
- if (!Array.isArray(content))
233
- return undefined;
234
- for (const part of content) {
235
- const text = part.text;
236
- if ('string' !== typeof text)
237
- continue;
238
- try {
239
- const parsed = JSON.parse(text);
240
- // Only a shape that actually carries a verdict — anything else stays `unknown`, which is the
241
- // honest answer for a result this function could not read.
242
- if ('object' === typeof parsed && null !== parsed && field in parsed) {
243
- return parsed;
244
- }
245
- }
246
- catch {
247
- /* Not JSON; the next part may still be. */
248
- }
249
- }
250
- return undefined;
251
- }
252
- /**
253
- * The tool's own words when it refused, rather than the envelope carrying them.
254
- *
255
- * A refusal from these tools is the most useful text they produce — it names the cause and the next
256
- * action — and printing the MCP result object around it buries that in JSON escaping. The reader is
257
- * at a terminal; give them the sentence.
258
- */
259
- export function refusalText(result) {
260
- const r = result;
261
- if (true !== r?.isError || !Array.isArray(r.content))
262
- return undefined;
263
- const text = r.content
264
- .map((part) => part.text)
265
- .filter((t) => 'string' === typeof t)
266
- .join('\n');
267
- if (0 === text.length)
268
- return undefined;
269
- // The tools answer with a JSON envelope whose `error` is the sentence; unwrap when it is one.
270
- try {
271
- const parsed = JSON.parse(text);
272
- const inner = parsed?.error;
273
- return 'string' === typeof inner ? inner : text;
274
- }
275
- catch {
276
- return text;
277
- }
278
- }
279
138
  /**
280
139
  * Navigate (optionally) and assert, against the daemon already on this port.
281
140
  *
@@ -315,8 +174,9 @@ export async function runAdhocVerdict(options) {
315
174
  }
316
175
  if (url !== undefined && options.sessionId === undefined && options.sessions !== undefined) {
317
176
  const opened = await leaseIfNoTab(caller, url, options.sessions, options.root);
318
- if ('failed' in opened)
177
+ if ('failed' in opened) {
319
178
  return { code: 1, lines: ['verified: unknown', ...opened.failed] };
179
+ }
320
180
  leased = opened.leased;
321
181
  alreadyAt = true === opened.alreadyAt;
322
182
  }
@@ -328,8 +188,9 @@ export async function runAdhocVerdict(options) {
328
188
  await caller.call(ReticleTool.NAVIGATE, { url, ...pin });
329
189
  }
330
190
  const waited = await waitForExpectation(caller, options.predicate, pin, options.timeoutMs);
331
- if ('refused' in waited)
191
+ if ('refused' in waited) {
332
192
  return { code: 1, lines: ['verified: unknown', waited.refused] };
193
+ }
333
194
  const result = waited.result;
334
195
  const refusal = refusalText(result);
335
196
  if (refusal !== undefined) {
@@ -357,8 +218,9 @@ export async function runAdhocVerdict(options) {
357
218
  };
358
219
  }
359
220
  finally {
360
- if (leased !== undefined)
221
+ if (leased !== undefined) {
361
222
  await releaseLease(caller, leased);
223
+ }
362
224
  await caller.close().catch(() => undefined);
363
225
  }
364
226
  }
@@ -384,8 +246,9 @@ export async function exploreToEnd(caller, args, budgetMs, now = Date.now) {
384
246
  for (;;) {
385
247
  const report = verdictOf(result, 'status');
386
248
  const runId = report?.['runId'];
387
- if (DRIVE_RUNNING !== report?.['status'] || 'string' !== typeof runId || now() >= deadline)
249
+ if (DRIVE_RUNNING !== report?.['status'] || 'string' !== typeof runId || now() >= deadline) {
388
250
  return result;
251
+ }
389
252
  result = await ask({ runId });
390
253
  }
391
254
  }
@@ -0,0 +1,2 @@
1
+ /** The same connection hint for a running daemon in `status` and `doctor`. */
2
+ export declare function mcpConnectionHint(port: number): string;
@@ -0,0 +1,6 @@
1
+ import { LOOPBACK_HOST, MCP_SSE_PATH } from '@reticlehq/core';
2
+ const MCP_CONNECTION_HINT = 'If your agent has no reticle_* tools, connect over HTTP/SSE at';
3
+ /** The same connection hint for a running daemon in `status` and `doctor`. */
4
+ export function mcpConnectionHint(port) {
5
+ return `${MCP_CONNECTION_HINT} http://${LOOPBACK_HOST}:${String(port)}${MCP_SSE_PATH}`;
6
+ }
@@ -73,7 +73,7 @@ function asIdentity(payload) {
73
73
  */
74
74
  function splitBrainLine(port, projectId) {
75
75
  const home = reticleStateHome();
76
- const elsewhere = splitBrainNote(port, daemonsServingProjectElsewhere(projectId, port, home, isAlive, (other) => hasProjectConnectedBefore(home, other, projectId)));
76
+ const elsewhere = splitBrainNote(port, daemonsServingProjectElsewhere(projectId, port, home, isAlive, (other) => hasProjectConnectedBefore(home, other, projectId)), { projectId, home });
77
77
  return elsewhere ?? wrongDaemonNote(port, resolveDaemonForProject(projectId, home, isAlive));
78
78
  }
79
79
  const writeLine = (s) => {
@@ -13,6 +13,7 @@ import { changedFilesSince } from '../../language/flows/change/git-changed.js';
13
13
  import { isAbsolute, join, sep } from 'node:path';
14
14
  import { ReticleDir } from '@reticlehq/core';
15
15
  import { FlowStore } from '../../language/flows/flows.js';
16
+ import { namedFlowsIn } from '../../language/flows/flow-store-for-session.js';
16
17
  import { RunStore } from '../../judgement/runs/artifact/run-store.js';
17
18
  import { createNodeFileSystem } from '../../memory/project/fs/fs-port.js';
18
19
  import { affectedSavedFlows, toFlowSources, } from '../../language/flows/change/flow-sources.js';
@@ -54,15 +55,7 @@ export async function resolveChangedFiles(files, since, cwd) {
54
55
  * confident negative about somebody else's project, which is worse than an error.
55
56
  */
56
57
  export async function loadNamedFlows(fs, reticleRoot, projectId) {
57
- const store = new FlowStore(fs, reticleRoot, { now: () => Date.now() });
58
- const flows = [];
59
- for (const name of await store.list(projectId)) {
60
- const loaded = await store.load(name, projectId);
61
- if (loaded.ok) {
62
- flows.push({ name: loaded.value.name, steps: loaded.value.steps });
63
- }
64
- }
65
- return flows;
58
+ return await namedFlowsIn(new FlowStore(fs, reticleRoot, { now: () => Date.now() }), projectId);
66
59
  }
67
60
  /**
68
61
  * `reticle gate <file...>` — exit non-zero unless passing artifacts cover the flows affected by the
@@ -69,7 +69,7 @@ export function planKill(input) {
69
69
  holder: listener,
70
70
  reason: `pid ${String(listener.pid)} ("${listener.command}") is listening, and it is not a Reticle ` +
71
71
  `daemon — it never answered /status and it is not the pid Reticle recorded. Killing it would ` +
72
- `be killing someone else's process. If that is what you meant, \`reticle restart ${FORCE_FLAG}\` ` +
72
+ `be killing someone else's process. If that is what you meant, \`reticle serve --restart ${FORCE_FLAG}\` ` +
73
73
  `stops it and starts Reticle there (\`stop ${FORCE_FLAG}\` never does); or start Reticle on ` +
74
74
  `another port.`,
75
75
  };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `reticle compare <reference-url> <candidate-url> [--base <url>] [--state <file>] [--json] [--report <file>]`. The
3
+ * command has one recipe. `--state` names the app's stored data to read, and `--port` names the
4
+ * daemon, as on every command that uses one.
5
+ *
6
+ * Split out of `cli-parse.ts` when that file came near the 1000-line cap.
7
+ */
8
+ import { type ParseError } from './cli-parse-grammar.js';
9
+ export declare const COMPARE_COMMAND = "compare";
10
+ export type CompareArgs = {
11
+ kind: 'ok';
12
+ reference: string;
13
+ candidate: string;
14
+ base?: string;
15
+ /** The path of the state sources file. */
16
+ state?: string;
17
+ /** Where to write the HTML report. */
18
+ report?: string;
19
+ json: boolean;
20
+ port: number;
21
+ } | ParseError;
22
+ export declare function parseCompareArgs(args: readonly string[], defaultPort: number): CompareArgs;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * `reticle compare <reference-url> <candidate-url> [--base <url>] [--state <file>] [--json] [--report <file>]`. The
3
+ * command has one recipe. `--state` names the app's stored data to read, and `--port` names the
4
+ * daemon, as on every command that uses one.
5
+ *
6
+ * Split out of `cli-parse.ts` when that file came near the 1000-line cap.
7
+ */
8
+ import { JSON_FLAG, PORT_FLAG, missingOperand, missingValue, notANumber, unknownArgument, } from './cli-parse-grammar.js';
9
+ export const COMPARE_COMMAND = 'compare';
10
+ /** `compare --base <url>`: the build before the change. */
11
+ const BASE_FLAG = '--base';
12
+ /** `compare --state <file.json>`: the declared state sources to read after each sequence. */
13
+ const STATE_FLAG = '--state';
14
+ /** `compare --report <file>`: also write the result as one HTML page. */
15
+ const REPORT_FLAG = '--report';
16
+ /** `compare` takes exactly a reference url and a candidate url. */
17
+ const COMPARE_URLS = 2;
18
+ export function parseCompareArgs(args, defaultPort) {
19
+ const urls = [];
20
+ let base;
21
+ let state;
22
+ let report;
23
+ let json = false;
24
+ let port = defaultPort;
25
+ for (let i = 0; i < args.length; i += 1) {
26
+ const arg = args[i] ?? '';
27
+ if (JSON_FLAG === arg) {
28
+ json = true;
29
+ continue;
30
+ }
31
+ if (BASE_FLAG === arg || STATE_FLAG === arg || REPORT_FLAG === arg || PORT_FLAG === arg) {
32
+ const value = args[i + 1];
33
+ if (value === undefined || value.startsWith('--')) {
34
+ return missingValue(arg);
35
+ }
36
+ i += 1;
37
+ if (BASE_FLAG === arg) {
38
+ base = value;
39
+ continue;
40
+ }
41
+ if (STATE_FLAG === arg) {
42
+ state = value;
43
+ continue;
44
+ }
45
+ if (REPORT_FLAG === arg) {
46
+ report = value;
47
+ continue;
48
+ }
49
+ const n = Number(value);
50
+ if (!Number.isInteger(n)) {
51
+ return notANumber(arg, value);
52
+ }
53
+ port = n;
54
+ continue;
55
+ }
56
+ if (arg.startsWith('--') || COMPARE_URLS === urls.length) {
57
+ return unknownArgument(arg);
58
+ }
59
+ urls.push(arg);
60
+ }
61
+ const [reference, candidate] = urls;
62
+ if (reference === undefined || candidate === undefined) {
63
+ return missingOperand(COMPARE_COMMAND, 'a reference url and a candidate url');
64
+ }
65
+ return {
66
+ kind: 'ok',
67
+ reference,
68
+ candidate,
69
+ ...(base === undefined ? {} : { base }),
70
+ ...(state === undefined ? {} : { state }),
71
+ ...(report === undefined ? {} : { report }),
72
+ json,
73
+ port,
74
+ };
75
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * `reticle grade <snapshot|judge|tables|suggest> [flags]`: grade a finished run of an app from
3
+ * snapshots of its stored data. The rules are in `@/features/grade/`.
4
+ */
5
+ import { type ParseError } from './cli-parse-grammar.js';
6
+ export declare const GRADE_COMMAND = "grade";
7
+ export declare const GradeStep: {
8
+ readonly SNAPSHOT: "snapshot";
9
+ readonly JUDGE: "judge";
10
+ readonly TABLES: "tables";
11
+ readonly SUGGEST: "suggest";
12
+ };
13
+ export type GradeStep = (typeof GradeStep)[keyof typeof GradeStep];
14
+ /** Each flag that takes a value, and the field it fills. */
15
+ declare const VALUE_FLAGS: {
16
+ readonly '--state': "state";
17
+ readonly '--out': "out";
18
+ readonly '--url': "url";
19
+ readonly '--reference': "reference";
20
+ readonly '--again': "again";
21
+ readonly '--candidate': "candidate";
22
+ readonly '--expect': "expect";
23
+ readonly '--answer': "answer";
24
+ readonly '--list': "list";
25
+ readonly '--rows': "rows";
26
+ readonly '--container': "container";
27
+ readonly '--skip': "skip";
28
+ };
29
+ type GradeField = (typeof VALUE_FLAGS)[keyof typeof VALUE_FLAGS];
30
+ export type GradeArgs = ({
31
+ kind: 'ok';
32
+ step: GradeStep;
33
+ json: boolean;
34
+ } & Partial<Record<GradeField, string>>) | ParseError;
35
+ export declare function parseGradeArgs(args: readonly string[]): GradeArgs;
36
+ export {};