@reticlehq/server 2.4.0 → 2.5.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 (416) hide show
  1. package/SKILL.md +702 -0
  2. package/dist/bridge/auth-failure-reason.d.ts +19 -0
  3. package/dist/bridge/auth-failure-reason.js +29 -0
  4. package/dist/{bridge-security.d.ts → bridge/bridge-security.d.ts} +1 -1
  5. package/dist/{bridge-security.js → bridge/bridge-security.js} +1 -1
  6. package/dist/{bridge.d.ts → bridge/bridge.d.ts} +1 -1
  7. package/dist/{bridge.js → bridge/bridge.js} +63 -13
  8. package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +1 -11
  9. package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +9 -9
  10. package/dist/{pairing-token.js → bridge/pairing-token.js} +1 -1
  11. package/dist/capsule/capsule-store.d.ts +8 -3
  12. package/dist/capsule/capsule-store.js +7 -2
  13. package/dist/capsule/capsule.js +3 -3
  14. package/dist/capsule/causal-summary.d.ts +8 -0
  15. package/dist/capsule/causal-summary.js +22 -12
  16. package/dist/capsule/divergence.d.ts +9 -4
  17. package/dist/capsule/divergence.js +8 -8
  18. package/dist/capsule/predicate-to-links.js +11 -8
  19. package/dist/{cli-doctor.js → cli/cli-doctor.js} +9 -2
  20. package/dist/{cli-flow-commands.d.ts → cli/cli-flow-commands.d.ts} +2 -2
  21. package/dist/{cli-flow-commands.js → cli/cli-flow-commands.js} +15 -15
  22. package/dist/{cli-launch.d.ts → cli/cli-launch.d.ts} +32 -3
  23. package/dist/cli/cli-launch.js +171 -0
  24. package/dist/{cli-parse.d.ts → cli/cli-parse.d.ts} +2 -11
  25. package/dist/{cli-parse.js → cli/cli-parse.js} +113 -60
  26. package/dist/{cli-port.d.ts → cli/cli-port.d.ts} +12 -0
  27. package/dist/{cli-port.js → cli/cli-port.js} +9 -9
  28. package/dist/cli/cli-update-commands.d.ts +4 -0
  29. package/dist/cli/cli-update-commands.js +93 -0
  30. package/dist/{cli-verify.js → cli/cli-verify.js} +14 -14
  31. package/dist/{cloud-cli.js → cli/cloud-cli.js} +29 -24
  32. package/dist/cli.d.ts +2 -2
  33. package/dist/cli.js +174 -92
  34. package/dist/cloud/cloud-config.d.ts +0 -1
  35. package/dist/cloud/cloud-config.js +9 -9
  36. package/dist/cloud/cloud-sync.d.ts +1 -7
  37. package/dist/cloud/cloud-sync.js +12 -12
  38. package/dist/crawl/crawl-empty.d.ts +16 -0
  39. package/dist/crawl/crawl-empty.js +24 -0
  40. package/dist/crawl/crawl-tools.js +4 -1
  41. package/dist/crawl/crawl.js +26 -7
  42. package/dist/daemon/daemon-resilience.d.ts +49 -0
  43. package/dist/daemon/daemon-resilience.js +191 -0
  44. package/dist/daemon/daemon-usefulness.d.ts +61 -0
  45. package/dist/daemon/daemon-usefulness.js +62 -0
  46. package/dist/{daemon.d.ts → daemon/daemon.d.ts} +33 -8
  47. package/dist/{daemon.js → daemon/daemon.js} +80 -48
  48. package/dist/daemon/idle-grace.d.ts +32 -0
  49. package/dist/daemon/idle-grace.js +41 -0
  50. package/dist/{idle-shutdown.d.ts → daemon/idle-shutdown.d.ts} +13 -0
  51. package/dist/{idle-shutdown.js → daemon/idle-shutdown.js} +27 -4
  52. package/dist/domain/domain-model.js +2 -2
  53. package/dist/events/body-failures.js +8 -8
  54. package/dist/events/contradictions.d.ts +1 -1
  55. package/dist/events/contradictions.js +60 -10
  56. package/dist/events/echo-mismatch.js +10 -10
  57. package/dist/events/event-filters.js +6 -6
  58. package/dist/events/json-salvage.d.ts +2 -1
  59. package/dist/events/json-salvage.js +6 -6
  60. package/dist/events/observed-in-window.d.ts +12 -0
  61. package/dist/events/observed-in-window.js +21 -0
  62. package/dist/events/predicate-eval.d.ts +43 -20
  63. package/dist/events/predicate-eval.js +159 -55
  64. package/dist/events/predicate.d.ts +2 -2
  65. package/dist/events/predicate.js +112 -35
  66. package/dist/events/reconcile.js +5 -5
  67. package/dist/events/ring-buffer.js +1 -1
  68. package/dist/events/stale-response.js +6 -6
  69. package/dist/events/testid-near-miss.d.ts +22 -0
  70. package/dist/events/testid-near-miss.js +33 -0
  71. package/dist/events/unit-mismatch.js +8 -8
  72. package/dist/flows/affected-tools.js +19 -4
  73. package/dist/flows/affected.js +1 -1
  74. package/dist/flows/annotate-target.d.ts +1 -0
  75. package/dist/flows/annotate-target.js +20 -0
  76. package/dist/flows/annotate-tools.js +25 -3
  77. package/dist/flows/annotate.js +17 -4
  78. package/dist/flows/assertion-integrity.d.ts +2 -1
  79. package/dist/flows/assertion-tiers-store.d.ts +3 -66
  80. package/dist/flows/assertion-tiers-store.js +2 -2
  81. package/dist/flows/attributed-failure.d.ts +17 -0
  82. package/dist/flows/attributed-failure.js +20 -0
  83. package/dist/flows/buddy-status.d.ts +2 -1
  84. package/dist/flows/buddy-status.js +2 -2
  85. package/dist/flows/coverage.d.ts +3 -2
  86. package/dist/flows/coverage.js +2 -2
  87. package/dist/flows/decision.js +24 -9
  88. package/dist/flows/empty-flow.d.ts +6 -0
  89. package/dist/flows/empty-flow.js +24 -0
  90. package/dist/flows/flake-store.d.ts +0 -1
  91. package/dist/flows/flake-store.js +1 -1
  92. package/dist/flows/flake.d.ts +2 -1
  93. package/dist/flows/flake.js +2 -2
  94. package/dist/flows/flow-classify.js +12 -0
  95. package/dist/flows/flow-replay-run.js +4 -4
  96. package/dist/flows/flow-replay.d.ts +5 -7
  97. package/dist/flows/flow-replay.js +114 -47
  98. package/dist/flows/flow-report.js +1 -1
  99. package/dist/flows/flow-scope.d.ts +1 -6
  100. package/dist/flows/flow-step-runners.d.ts +26 -6
  101. package/dist/flows/flow-step-runners.js +157 -6
  102. package/dist/flows/flow-success.js +20 -15
  103. package/dist/flows/flow-tools.js +53 -7
  104. package/dist/flows/flows.js +15 -11
  105. package/dist/flows/gate.d.ts +4 -3
  106. package/dist/flows/gate.js +1 -1
  107. package/dist/flows/heal-run.d.ts +0 -8
  108. package/dist/flows/heal-run.js +5 -5
  109. package/dist/flows/heal.js +3 -3
  110. package/dist/flows/predicate-to-expect.d.ts +38 -0
  111. package/dist/flows/predicate-to-expect.js +95 -0
  112. package/dist/flows/replay.d.ts +16 -1
  113. package/dist/flows/replay.js +58 -22
  114. package/dist/flows/role-anchor-nearest.d.ts +6 -0
  115. package/dist/flows/role-anchor-nearest.js +45 -0
  116. package/dist/flows/server-verify.d.ts +1 -1
  117. package/dist/flows/server-verify.js +5 -5
  118. package/dist/flows/verify-change-tools.js +32 -10
  119. package/dist/flows/verify-change-verdict.d.ts +15 -0
  120. package/dist/flows/verify-change-verdict.js +25 -0
  121. package/dist/flows/watch-batcher.d.ts +3 -2
  122. package/dist/honesty/already-true.d.ts +2 -0
  123. package/dist/honesty/already-true.js +31 -0
  124. package/dist/honesty/blind-spots.d.ts +16 -3
  125. package/dist/honesty/blind-spots.js +24 -12
  126. package/dist/honesty/honesty.d.ts +3 -2
  127. package/dist/honesty/honesty.js +4 -4
  128. package/dist/honesty/uncaptured-bodies.js +1 -1
  129. package/dist/honesty/unread-outcome.js +2 -2
  130. package/dist/honesty/verified.d.ts +22 -3
  131. package/dist/honesty/verified.js +48 -8
  132. package/dist/http-server.js +14 -7
  133. package/dist/hunt/hunt-report.d.ts +2 -1
  134. package/dist/hunt/hunt-report.js +2 -2
  135. package/dist/index.d.ts +8 -6
  136. package/dist/index.js +32 -25
  137. package/dist/init/agent-rules.d.ts +2 -1
  138. package/dist/init/agent-rules.js +45 -7
  139. package/dist/init/app-choice.d.ts +20 -0
  140. package/dist/init/app-choice.js +25 -0
  141. package/dist/init/astro-host.d.ts +21 -0
  142. package/dist/init/astro-host.js +27 -0
  143. package/dist/init/astro-patch.d.ts +16 -0
  144. package/dist/init/astro-patch.js +240 -0
  145. package/dist/init/capabilities.js +4 -1
  146. package/dist/init/cra.d.ts +37 -0
  147. package/dist/init/cra.js +90 -0
  148. package/dist/init/cursor.js +39 -4
  149. package/dist/init/desktop-doctor.d.ts +3 -2
  150. package/dist/init/desktop-doctor.js +4 -4
  151. package/dist/init/detect.d.ts +3 -2
  152. package/dist/init/detect.js +16 -5
  153. package/dist/init/mcp-registered.d.ts +12 -0
  154. package/dist/init/mcp-registered.js +14 -0
  155. package/dist/init/mcp.d.ts +27 -2
  156. package/dist/init/mcp.js +40 -4
  157. package/dist/init/next-patch.d.ts +2 -4
  158. package/dist/init/next-patch.js +109 -19
  159. package/dist/init/node-io.js +23 -6
  160. package/dist/init/plan-framework.d.ts +16 -1
  161. package/dist/init/plan-framework.js +132 -8
  162. package/dist/init/plan.d.ts +73 -8
  163. package/dist/init/plan.js +133 -34
  164. package/dist/init/project-id.d.ts +5 -9
  165. package/dist/init/project-id.js +10 -22
  166. package/dist/init/run.d.ts +5 -0
  167. package/dist/init/run.js +210 -36
  168. package/dist/init/slash-command.d.ts +9 -0
  169. package/dist/init/slash-command.js +10 -1
  170. package/dist/init/snippets.d.ts +7 -10
  171. package/dist/init/snippets.js +44 -15
  172. package/dist/init/vite-config.js +1 -1
  173. package/dist/init/workspace-apps.d.ts +23 -0
  174. package/dist/init/workspace-apps.js +68 -0
  175. package/dist/input/driven-redaction.js +1 -1
  176. package/dist/input/network-detail.js +8 -8
  177. package/dist/input/network-mock-tools.js +2 -2
  178. package/dist/input/network-mock.js +4 -4
  179. package/dist/input/real-input.js +8 -3
  180. package/dist/input/scroll-find.js +2 -2
  181. package/dist/input/scroll-tools.d.ts +0 -5
  182. package/dist/input/scroll-tools.js +11 -4
  183. package/dist/input/viewport-tools.js +2 -2
  184. package/dist/journal/ambient-store.d.ts +0 -2
  185. package/dist/journal/ambient-store.js +1 -1
  186. package/dist/journal/ambient.js +1 -1
  187. package/dist/journal/attach-journal.d.ts +2 -1
  188. package/dist/journal/deviation-report.js +6 -6
  189. package/dist/journal/deviation-service.js +1 -1
  190. package/dist/journal/envelope-store.d.ts +0 -2
  191. package/dist/journal/envelope-store.js +1 -1
  192. package/dist/journal/envelope.d.ts +2 -3
  193. package/dist/journal/envelope.js +3 -3
  194. package/dist/journal/journal-query.js +1 -1
  195. package/dist/journal/journal-recorder.d.ts +2 -1
  196. package/dist/journal/journal-recorder.js +2 -2
  197. package/dist/journal/rollups.js +9 -9
  198. package/dist/journal/session-end.d.ts +2 -1
  199. package/dist/journal/session-journal.js +4 -4
  200. package/dist/license/license.d.ts +0 -14
  201. package/dist/license/license.js +8 -8
  202. package/dist/log.d.ts +10 -0
  203. package/dist/log.js +11 -1
  204. package/dist/mcp/mcp-is-error.d.ts +13 -0
  205. package/dist/mcp/mcp-is-error.js +18 -0
  206. package/dist/mcp/mcp-outage.d.ts +31 -0
  207. package/dist/mcp/mcp-outage.js +48 -0
  208. package/dist/mcp/mcp-proxy.d.ts +124 -0
  209. package/dist/mcp/mcp-proxy.js +697 -0
  210. package/dist/{mcp.d.ts → mcp/mcp.d.ts} +13 -10
  211. package/dist/{mcp.js → mcp/mcp.js} +110 -35
  212. package/dist/mcp/proxy-handshake.d.ts +3 -0
  213. package/dist/mcp/proxy-handshake.js +54 -0
  214. package/dist/mcp/proxy-lifecycle.d.ts +64 -0
  215. package/dist/mcp/proxy-lifecycle.js +70 -0
  216. package/dist/mcp/tool-catalog-cache.d.ts +29 -0
  217. package/dist/mcp/tool-catalog-cache.js +55 -0
  218. package/dist/oracles/propose-consequences.d.ts +2 -1
  219. package/dist/oracles/propose-consequences.js +16 -11
  220. package/dist/oracles/self-instrument.d.ts +2 -1
  221. package/dist/oracles/self-instrument.js +1 -1
  222. package/dist/phenomena/phenomena.js +3 -3
  223. package/dist/platform.d.ts +16 -0
  224. package/dist/platform.js +16 -0
  225. package/dist/pool/playwright-launcher.d.ts +15 -2
  226. package/dist/pool/playwright-launcher.js +21 -2
  227. package/dist/project/fs-port.js +1 -1
  228. package/dist/project/project-tools.js +2 -2
  229. package/dist/runs/build-verification-run.js +1 -1
  230. package/dist/runs/repair-prompt.d.ts +0 -6
  231. package/dist/runs/repair-prompt.js +3 -3
  232. package/dist/runs/risk-classify.js +1 -1
  233. package/dist/runs/run-diff.d.ts +0 -2
  234. package/dist/runs/run-diff.js +3 -3
  235. package/dist/runs/run-tools.js +2 -2
  236. package/dist/runs/verification-sync.js +2 -2
  237. package/dist/runs/verify-http.js +1 -1
  238. package/dist/runs/verify-server.js +1 -1
  239. package/dist/session/command-timeout.d.ts +11 -3
  240. package/dist/session/command-timeout.js +2 -2
  241. package/dist/session/control-envelope.d.ts +6 -0
  242. package/dist/session/control-envelope.js +7 -1
  243. package/dist/session/dev-server-probe.d.ts +13 -0
  244. package/dist/session/dev-server-probe.js +52 -0
  245. package/dist/session/last-act.d.ts +17 -3
  246. package/dist/session/last-act.js +16 -4
  247. package/dist/session/live-control-tools.js +7 -2
  248. package/dist/session/live-control.js +1 -1
  249. package/dist/session/looks-like-dev-server.d.ts +14 -0
  250. package/dist/session/looks-like-dev-server.js +25 -0
  251. package/dist/session/no-session-diagnosis.d.ts +38 -0
  252. package/dist/session/no-session-diagnosis.js +70 -0
  253. package/dist/session/no-session-watch.d.ts +19 -0
  254. package/dist/session/no-session-watch.js +61 -0
  255. package/dist/session/observed-state.d.ts +4 -0
  256. package/dist/session/observed-state.js +19 -1
  257. package/dist/session/output-budget.js +1 -1
  258. package/dist/session/session-health.d.ts +8 -1
  259. package/dist/session/session-health.js +8 -7
  260. package/dist/session/session-info.d.ts +26 -7
  261. package/dist/session/session-info.js +42 -1
  262. package/dist/session/session-lease.d.ts +0 -11
  263. package/dist/session/session-lease.js +1 -1
  264. package/dist/session/session-manager.d.ts +3 -0
  265. package/dist/session/session-manager.js +52 -9
  266. package/dist/session/session-readiness.d.ts +1 -1
  267. package/dist/session/session-readiness.js +6 -1
  268. package/dist/session/session-reaper.js +1 -1
  269. package/dist/session/session-reconnect.d.ts +32 -0
  270. package/dist/session/session-reconnect.js +42 -0
  271. package/dist/session/session.d.ts +14 -2
  272. package/dist/session/session.js +69 -26
  273. package/dist/session/state-select.d.ts +1 -1
  274. package/dist/session/state-select.js +1 -1
  275. package/dist/status-payload.d.ts +19 -0
  276. package/dist/status-payload.js +11 -0
  277. package/dist/telemetry/argument-shape.js +10 -1
  278. package/dist/telemetry/browser-mode.d.ts +27 -0
  279. package/dist/telemetry/browser-mode.js +33 -0
  280. package/dist/telemetry/bug-found.d.ts +17 -21
  281. package/dist/telemetry/bug-found.js +120 -8
  282. package/dist/telemetry/cli-telemetry.d.ts +2 -12
  283. package/dist/telemetry/cli-telemetry.js +27 -4
  284. package/dist/telemetry/daemon-telemetry.d.ts +19 -0
  285. package/dist/telemetry/daemon-telemetry.js +12 -2
  286. package/dist/telemetry/dev-repo.js +4 -4
  287. package/dist/telemetry/error-fingerprint.js +21 -2
  288. package/dist/telemetry/feedback-cli.d.ts +1 -1
  289. package/dist/telemetry/feedback-cli.js +4 -4
  290. package/dist/telemetry/feedback-context.d.ts +22 -0
  291. package/dist/telemetry/feedback-context.js +41 -1
  292. package/dist/telemetry/feedback-delivery.d.ts +20 -0
  293. package/dist/telemetry/feedback-delivery.js +30 -0
  294. package/dist/telemetry/feedback-outbox.d.ts +22 -0
  295. package/dist/telemetry/feedback-outbox.js +87 -0
  296. package/dist/telemetry/feedback.d.ts +22 -1
  297. package/dist/telemetry/feedback.js +67 -8
  298. package/dist/telemetry/git-facts.js +1 -1
  299. package/dist/telemetry/session-metrics.d.ts +2 -0
  300. package/dist/telemetry/session-metrics.js +68 -6
  301. package/dist/telemetry/telemetry.d.ts +27 -2
  302. package/dist/telemetry/telemetry.js +119 -29
  303. package/dist/telemetry/verification-of.d.ts +22 -0
  304. package/dist/telemetry/verification-of.js +84 -0
  305. package/dist/tools/act-capsule.js +2 -2
  306. package/dist/tools/act-danger.d.ts +12 -0
  307. package/dist/tools/act-danger.js +20 -3
  308. package/dist/tools/act-helpers.js +4 -4
  309. package/dist/tools/act-tools.d.ts +0 -8
  310. package/dist/tools/act-tools.js +84 -115
  311. package/dist/tools/act-view.js +7 -7
  312. package/dist/tools/alias-args.d.ts +17 -0
  313. package/dist/tools/alias-args.js +25 -0
  314. package/dist/tools/assert-grade.js +9 -9
  315. package/dist/tools/assert-verdict.d.ts +3 -1
  316. package/dist/tools/assert-verdict.js +8 -5
  317. package/dist/tools/browser-tools.js +30 -11
  318. package/dist/tools/contract-tools.js +1 -1
  319. package/dist/tools/coverage-identity.d.ts +34 -0
  320. package/dist/tools/coverage-identity.js +46 -0
  321. package/dist/tools/coverage-tools.js +10 -14
  322. package/dist/tools/dynamic-tools.d.ts +2 -1
  323. package/dist/tools/dynamic-tools.js +90 -18
  324. package/dist/tools/error-recovery.d.ts +20 -2
  325. package/dist/tools/error-recovery.js +179 -4
  326. package/dist/tools/feedback-tools.d.ts +12 -1
  327. package/dist/tools/feedback-tools.js +28 -5
  328. package/dist/tools/invoke-tool.d.ts +0 -6
  329. package/dist/tools/invoke-tool.js +57 -32
  330. package/dist/tools/lease-hint.d.ts +17 -0
  331. package/dist/tools/lease-hint.js +24 -0
  332. package/dist/tools/lease-tools.js +6 -5
  333. package/dist/tools/merge-tools.d.ts +10 -1
  334. package/dist/tools/merge-tools.js +9 -3
  335. package/dist/tools/merged-name-redirect.d.ts +13 -0
  336. package/dist/tools/merged-name-redirect.js +55 -0
  337. package/dist/tools/navigate-result.d.ts +17 -0
  338. package/dist/tools/navigate-result.js +31 -0
  339. package/dist/tools/observe-tools.js +54 -20
  340. package/dist/tools/observed-nothing.d.ts +25 -0
  341. package/dist/tools/observed-nothing.js +33 -0
  342. package/dist/tools/query-paginate.js +2 -2
  343. package/dist/tools/query-shape.d.ts +18 -0
  344. package/dist/tools/query-shape.js +39 -0
  345. package/dist/tools/read-tools.js +81 -20
  346. package/dist/tools/real-input-attempt.d.ts +28 -0
  347. package/dist/tools/real-input-attempt.js +88 -0
  348. package/dist/tools/reconcile-tools.js +7 -7
  349. package/dist/tools/reload-result.d.ts +14 -0
  350. package/dist/tools/reload-result.js +29 -0
  351. package/dist/tools/snapshot-delta.d.ts +4 -5
  352. package/dist/tools/snapshot-delta.js +0 -0
  353. package/dist/tools/tool-kit.d.ts +8 -0
  354. package/dist/tools/tool-surface.d.ts +83 -0
  355. package/dist/tools/tool-surface.js +204 -0
  356. package/dist/tools/tools-helpers.js +6 -6
  357. package/dist/tools/tools.d.ts +15 -0
  358. package/dist/tools/tools.js +53 -17
  359. package/dist/tools/unadvertised-help.d.ts +5 -0
  360. package/dist/tools/unadvertised-help.js +39 -0
  361. package/dist/trace.d.ts +69 -0
  362. package/dist/trace.js +150 -0
  363. package/dist/update/nudge-credit.d.ts +4 -0
  364. package/dist/update/nudge-credit.js +53 -0
  365. package/dist/update/sdk-sync.d.ts +21 -0
  366. package/dist/update/sdk-sync.js +42 -0
  367. package/dist/update/update-checker.js +3 -3
  368. package/dist/update/update-nudge.d.ts +63 -6
  369. package/dist/update/update-nudge.js +128 -15
  370. package/dist/update/updater.d.ts +1 -9
  371. package/dist/update/updater.js +13 -8
  372. package/dist/version/peer-announce.d.ts +4 -0
  373. package/dist/version/peer-announce.js +30 -0
  374. package/dist/{server-version.js → version/server-version.js} +5 -1
  375. package/dist/version/version-nudge.d.ts +28 -0
  376. package/dist/version/version-nudge.js +40 -0
  377. package/dist/version/version-skew.d.ts +66 -0
  378. package/dist/version/version-skew.js +71 -0
  379. package/dist/visual/visual-diff.js +2 -2
  380. package/dist/visual/visual-tools.js +4 -4
  381. package/docs/agent-cheatsheet.md +85 -0
  382. package/docs/architecture.md +113 -0
  383. package/docs/benchmarks.md +255 -0
  384. package/docs/debugging.md +102 -0
  385. package/docs/deploy-checks.md +69 -0
  386. package/docs/desktop-apps.md +256 -0
  387. package/docs/enterprise.md +62 -0
  388. package/docs/fixtures.md +28 -0
  389. package/docs/flows.md +166 -0
  390. package/docs/getting-started.md +469 -0
  391. package/docs/human-control.md +63 -0
  392. package/docs/integration-patterns.md +171 -0
  393. package/docs/local-registry.md +126 -0
  394. package/docs/mint.json +61 -0
  395. package/docs/multi-agent-testing.md +55 -0
  396. package/docs/platform-integration.md +122 -0
  397. package/docs/telemetry-contract.md +214 -0
  398. package/docs/telemetry.md +184 -0
  399. package/docs/testing.md +82 -0
  400. package/docs/token-efficiency.md +80 -0
  401. package/docs/usage.md +1046 -0
  402. package/package.json +8 -5
  403. package/dist/cli-launch.js +0 -112
  404. package/dist/daemon-resilience.d.ts +0 -22
  405. package/dist/daemon-resilience.js +0 -85
  406. package/dist/mcp-proxy.d.ts +0 -67
  407. package/dist/mcp-proxy.js +0 -337
  408. package/dist/tools/profiles.d.ts +0 -37
  409. package/dist/tools/profiles.js +0 -139
  410. /package/dist/{pairing-token.d.ts → bridge/pairing-token.d.ts} +0 -0
  411. /package/dist/{token-auth.d.ts → bridge/token-auth.d.ts} +0 -0
  412. /package/dist/{token-auth.js → bridge/token-auth.js} +0 -0
  413. /package/dist/{cli-doctor.d.ts → cli/cli-doctor.d.ts} +0 -0
  414. /package/dist/{cli-verify.d.ts → cli/cli-verify.d.ts} +0 -0
  415. /package/dist/{cloud-cli.d.ts → cli/cloud-cli.d.ts} +0 -0
  416. /package/dist/{server-version.d.ts → version/server-version.d.ts} +0 -0
@@ -1,84 +1,147 @@
1
- import { ElementQuerySchema, ElementState, EventType, } from '@reticlehq/core';
1
+ import { ElementQuerySchema, ElementState, EventType, PredicateKind, StreamDirection, isDevToolingUrl, } from '@reticlehq/core';
2
2
  import { z } from 'zod';
3
- export const PredicateSchema = z.lazy(() => z.discriminatedUnion('kind', [
4
- z.object({
5
- kind: z.literal('element'),
3
+ import { describeObserved } from './observed-in-window.js';
4
+ /**
5
+ * Spellings an agent plausibly reaches for, mapped to the real field.
6
+ *
7
+ * `route` spells its field `pathname` while `state`, in the same union, spells its `path`. Every one
8
+ * of these was silently DROPPED before, and because these kinds have all-optional fields, dropping
9
+ * the only key supplied left a predicate that asserts nothing and passes on anything.
10
+ */
11
+ const PREDICATE_ALIASES = {
12
+ [PredicateKind.ROUTE]: { path: 'pathname' },
13
+ [PredicateKind.NET]: { url: 'urlContains' },
14
+ [PredicateKind.SIGNAL]: { data: 'dataMatches' },
15
+ };
16
+ /** Rename known aliases before parse; an explicit canonical key always wins. */
17
+ function applyPredicateAliases(input) {
18
+ if (typeof input !== 'object' || null === input || Array.isArray(input))
19
+ return input;
20
+ const obj = input;
21
+ const aliases = PREDICATE_ALIASES['string' === typeof obj['kind'] ? obj['kind'] : ''];
22
+ if (aliases === undefined)
23
+ return input;
24
+ const out = { ...obj };
25
+ for (const [from, to] of Object.entries(aliases)) {
26
+ if (out[from] === undefined)
27
+ continue;
28
+ if (out[to] === undefined)
29
+ out[to] = out[from];
30
+ delete out[from];
31
+ }
32
+ return out;
33
+ }
34
+ /**
35
+ * Strict on every branch. A key that is nobody's spelling is now a schema error naming it, instead of
36
+ * a stripped field and a green — see predicate-strict.test.ts for the MCP session that found this.
37
+ */
38
+ export const PredicateSchema = z.lazy(() => z.preprocess(applyPredicateAliases, z.discriminatedUnion('kind', [
39
+ z
40
+ .object({
41
+ kind: z.literal(PredicateKind.ELEMENT),
6
42
  query: ElementQuerySchema,
7
43
  state: z.nativeEnum(ElementState).optional(),
8
44
  absent: z.boolean().optional(),
9
- }),
10
- z.object({
11
- kind: z.literal('text'),
45
+ })
46
+ .strict(),
47
+ z
48
+ .object({
49
+ kind: z.literal(PredicateKind.TEXT),
12
50
  contains: z.string(),
13
51
  visible: z.boolean().optional(),
14
52
  absent: z.boolean().optional(),
15
- }),
16
- z.object({
17
- kind: z.literal('net'),
53
+ })
54
+ .strict(),
55
+ z
56
+ .object({
57
+ kind: z.literal(PredicateKind.NET),
18
58
  method: z.string().optional(),
19
59
  urlContains: z.string().optional(),
20
60
  status: z.number().optional(),
21
61
  ok: z.boolean().optional(),
22
62
  since: z.number().optional(),
23
63
  count: z.number().int().nonnegative().optional(),
24
- }),
25
- z.object({
26
- kind: z.literal('route'),
64
+ })
65
+ .strict(),
66
+ z
67
+ .object({
68
+ kind: z.literal(PredicateKind.ROUTE),
27
69
  pathname: z.string().optional(),
28
70
  contains: z.string().optional(),
29
- }),
30
- z.object({
31
- kind: z.literal('console'),
71
+ since: z.number().optional(),
72
+ })
73
+ .strict(),
74
+ z
75
+ .object({
76
+ kind: z.literal(PredicateKind.CONSOLE),
32
77
  level: z.string().optional(),
33
78
  absent: z.boolean().optional(),
34
79
  since: z.number().optional(),
35
- }),
36
- z.object({
37
- kind: z.literal('animation'),
80
+ })
81
+ .strict(),
82
+ z
83
+ .object({
84
+ kind: z.literal(PredicateKind.ANIMATION),
38
85
  name: z.string().optional(),
39
86
  target: z.string().optional(),
40
87
  completed: z.boolean().optional(),
41
- }),
42
- z.object({
43
- kind: z.literal('signal'),
88
+ since: z.number().optional(),
89
+ })
90
+ .strict(),
91
+ z
92
+ .object({
93
+ kind: z.literal(PredicateKind.SIGNAL),
44
94
  name: z.string().optional(),
45
95
  dataMatches: z.record(z.unknown()).optional(),
46
- }),
47
- z.object({
48
- kind: z.literal('state'),
96
+ since: z.number().optional(),
97
+ })
98
+ .strict(),
99
+ z
100
+ .object({
101
+ kind: z.literal(PredicateKind.STATE),
49
102
  store: z.string().optional(),
50
103
  path: z.string(),
51
104
  equals: z.unknown().optional(),
52
- }),
53
- z.object({ kind: z.literal('settled'), quietMs: z.number().positive().optional() }),
54
- z.object({ kind: z.literal('allOf'), predicates: z.array(PredicateSchema) }),
55
- z.object({ kind: z.literal('anyOf'), predicates: z.array(PredicateSchema) }),
56
- z.object({ kind: z.literal('not'), predicate: PredicateSchema }),
57
- ]));
105
+ })
106
+ .strict(),
107
+ z
108
+ .object({
109
+ kind: z.literal(PredicateKind.SETTLED),
110
+ quietMs: z.number().positive().optional(),
111
+ })
112
+ .strict(),
113
+ z
114
+ .object({ kind: z.literal(PredicateKind.ALL_OF), predicates: z.array(PredicateSchema) })
115
+ .strict(),
116
+ z
117
+ .object({ kind: z.literal(PredicateKind.ANY_OF), predicates: z.array(PredicateSchema) })
118
+ .strict(),
119
+ z.object({ kind: z.literal(PredicateKind.NOT), predicate: PredicateSchema }).strict(),
120
+ ])));
58
121
  function str(value) {
59
- return typeof value === 'string' ? value : undefined;
122
+ return 'string' === typeof value ? value : undefined;
60
123
  }
61
124
  function num(value) {
62
- return typeof value === 'number' ? value : undefined;
125
+ return 'number' === typeof value ? value : undefined;
63
126
  }
64
127
  /**
65
128
  * Match one value against a pattern. Supports `*` (present), strict equality, and operators:
66
129
  * `{$gte,$lte,$gt,$lt}` (numbers), `{$contains}` (array membership or substring), `{$length}`.
67
130
  */
68
131
  export function matchValue(got, want) {
69
- if (want === '*')
132
+ if ('*' === want)
70
133
  return got !== undefined;
71
134
  // An object is an OPERATOR container only if it actually carries a `$`-prefixed operator. An empty
72
135
  // `{}` (or an object with no `$` key) used to enter this branch, iterate zero recognized operators,
73
136
  // and `return true` — so `equals: {}` / `dataMatches: {status: {}}` was a green assertion that
74
137
  // passed against ANYTHING, undefined included: the exact false green the oracle exists to catch.
75
138
  // Without an operator it is a literal to compare, and falls through to strict equality below.
76
- const ops = typeof want === 'object' && want !== null && !Array.isArray(want)
139
+ const ops = 'object' === typeof want && want !== null && !Array.isArray(want)
77
140
  ? Object.entries(want)
78
141
  : undefined;
79
142
  if (ops !== undefined && ops.some(([op]) => op.startsWith('$'))) {
80
143
  for (const [op, val] of ops) {
81
- const n = typeof got === 'number' ? got : NaN;
144
+ const n = 'number' === typeof got ? got : NaN;
82
145
  switch (op) {
83
146
  case '$gte':
84
147
  if (!(n >= val))
@@ -101,7 +164,7 @@ export function matchValue(got, want) {
101
164
  if (!got.includes(val))
102
165
  return false;
103
166
  }
104
- else if (typeof got === 'string') {
167
+ else if ('string' === typeof got) {
105
168
  if (!got.includes(String(val)))
106
169
  return false;
107
170
  }
@@ -110,7 +173,7 @@ export function matchValue(got, want) {
110
173
  }
111
174
  break;
112
175
  case '$length':
113
- if (!((Array.isArray(got) || typeof got === 'string') && got.length === val)) {
176
+ if (!((Array.isArray(got) || 'string' === typeof got) && got.length === val)) {
114
177
  return false;
115
178
  }
116
179
  break;
@@ -140,7 +203,7 @@ function dataMatches(actual, pattern) {
140
203
  * This exists so an agent can assert on the OUTCOME rather than on a number Reticle invented.
141
204
  */
142
205
  function callSucceeded(data) {
143
- if (typeof data['ok'] === 'boolean')
206
+ if ('boolean' === typeof data['ok'])
144
207
  return data['ok'];
145
208
  const status = num(data['status']);
146
209
  return status === undefined || status < 400;
@@ -183,7 +246,11 @@ export function evalNet(events, p) {
183
246
  : {
184
247
  pass: false,
185
248
  failureReason: `no network call matched ${JSON.stringify(p)}`,
186
- observed: 'no matching network call in the window',
249
+ // Same reasoning as the signal miss: "no matching call" cannot be told apart from "the app
250
+ // made no calls at all", and those need different fixes.
251
+ observed: describeObserved('calls', events
252
+ .filter((e) => e.type === EventType.NET_REQUEST)
253
+ .map((e) => `${str(e.data['method']) ?? 'GET'} ${str(e.data['url']) ?? ''}`)),
187
254
  expected: `at least one call matching ${JSON.stringify(p)}`,
188
255
  assertion: 'net.present',
189
256
  };
@@ -257,17 +324,17 @@ export function evalConsole(events, p) {
257
324
  e.type === EventType.CONSOLE_ERROR ||
258
325
  e.type === EventType.ERROR_UNCAUGHT);
259
326
  }
260
- if (p.level === 'error')
327
+ if ('error' === p.level)
261
328
  return isErr;
262
329
  return e.type === CONSOLE_LEVEL_TYPE[p.level];
263
330
  });
264
- if (p.absent === true) {
265
- return matches.length === 0
331
+ if (true === p.absent) {
332
+ return 0 === matches.length
266
333
  ? { pass: true, evidence: { absent: true } }
267
334
  : {
268
335
  pass: false,
269
336
  failureReason: `expected no ${p.level ?? 'console'} entries but found ${String(matches.length)}`,
270
- observed: `${String(matches.length)} ${p.level ?? 'console'} entr${matches.length === 1 ? 'y' : 'ies'}`,
337
+ observed: `${String(matches.length)} ${p.level ?? 'console'} entr${1 === matches.length ? 'y' : 'ies'}`,
271
338
  expected: `no ${p.level ?? 'console'} entries`,
272
339
  assertion: 'console.absent',
273
340
  evidence: matches.map((e) => e.data),
@@ -284,7 +351,7 @@ export function evalConsole(events, p) {
284
351
  };
285
352
  }
286
353
  export function evalAnimation(events, p) {
287
- const wantType = p.completed === true ? EventType.ANIM_END : EventType.ANIM_START;
354
+ const wantType = true === p.completed ? EventType.ANIM_END : EventType.ANIM_START;
288
355
  const hit = events.find((e) => {
289
356
  if (e.type !== wantType)
290
357
  return false;
@@ -330,7 +397,9 @@ export function evalSignal(events, p) {
330
397
  : `no signal matched ${JSON.stringify(p)}`,
331
398
  observed: sameName.length > 0
332
399
  ? `signal '${p.name ?? '(any)'}' fired ${String(sameName.length)}x, payload: ${JSON.stringify(sameName[0])}`
333
- : `signal '${p.name ?? '(any)'}' never fired in the window`,
400
+ : // Name what DID fire: a typo'd signal name and a genuinely dead action produce the same
401
+ // sentence otherwise, and the agent cannot tell them apart. See observed-in-window.ts.
402
+ `signal '${p.name ?? '(any)'}' never fired — ${describeObserved('signals', events.filter((e) => e.type === EventType.SIGNAL).map((e) => str(e.data['name']) ?? ''))}`,
334
403
  expected: p.dataMatches === undefined
335
404
  ? `signal '${p.name ?? '(any)'}' to fire`
336
405
  : `signal '${p.name ?? '(any)'}' with payload matching ${JSON.stringify(p.dataMatches)}`,
@@ -364,6 +433,12 @@ const SETTLE_ACTIVITY = new Set([
364
433
  EventType.DOM_REMOVED,
365
434
  EventType.DOM_ATTR,
366
435
  ]);
436
+ /** The three net-shaped event types a URL can be read off — the only ones dev tooling can produce. */
437
+ const NET_TYPES = new Set([
438
+ EventType.NET_PENDING,
439
+ EventType.NET_REQUEST,
440
+ EventType.NET_STREAM,
441
+ ]);
367
442
  /** Default quiet window — enough to absorb a render+xhr settle without waiting on slow polls. */
368
443
  const DEFAULT_QUIET_MS = 500;
369
444
  /**
@@ -372,8 +447,24 @@ const DEFAULT_QUIET_MS = 500;
372
447
  * now, not to any buffered event — so `now` is injected (CLAUDE.md rule 7), and the wait loop's
373
448
  * poll interval is what eventually flips this to pass once activity stops.
374
449
  */
375
- export function evalSettled(events, p, now) {
450
+ export function evalSettled(allEvents, p, now) {
376
451
  const quietMs = p.quietMs ?? DEFAULT_QUIET_MS;
452
+ // Dev-tooling traffic is the framework talking about ITSELF (see DevToolingChannel) and says
453
+ // nothing about whether the app finished its work — but it was holding settle open. Dropped from
454
+ // both halves of the calculation below (in-flight AND the quiet timer) and DISCLOSED on the
455
+ // evidence, never silently swallowed.
456
+ const ignoredDevTooling = [];
457
+ const events = allEvents.filter((e) => {
458
+ if (!NET_TYPES.has(e.type))
459
+ return true;
460
+ const url = str(e.data['url']);
461
+ if (!isDevToolingUrl(url))
462
+ return true;
463
+ if (url !== undefined && !ignoredDevTooling.includes(url))
464
+ ignoredDevTooling.push(url);
465
+ return false;
466
+ });
467
+ const disclosure = 0 === ignoredDevTooling.length ? {} : { ignoredDevTooling };
377
468
  // A request that STARTED (NET_PENDING) but never completed (NET_REQUEST with the same id) is
378
469
  // still in flight — the page is NOT settled no matter how quiet the DOM has gone. Without this,
379
470
  // a slow save reads as "settled" the instant its spinner stops mutating the DOM: the exact
@@ -417,16 +508,16 @@ export function evalSettled(events, p, now) {
417
508
  if (id === undefined)
418
509
  continue;
419
510
  const direction = str(e.data['direction']);
420
- if (direction === 'open')
511
+ if (StreamDirection.OPEN === direction)
421
512
  openStreams.add(id);
422
- else if (direction === 'close')
513
+ else if (StreamDirection.CLOSE === direction)
423
514
  openStreams.delete(id);
424
515
  }
425
516
  const streaming = openStreams.size;
426
517
  if (inFlight + streaming > 0) {
427
- const what = streaming === 0
518
+ const what = 0 === streaming
428
519
  ? `${String(inFlight)} request(s) still in flight`
429
- : inFlight === 0
520
+ : 0 === inFlight
430
521
  ? `${String(streaming)} response body(ies) still streaming`
431
522
  : `${String(inFlight)} request(s) in flight and ${String(streaming)} response body(ies) still streaming`;
432
523
  return {
@@ -435,7 +526,12 @@ export function evalSettled(events, p, now) {
435
526
  observed: what,
436
527
  expected: 'no requests in flight and no response bodies still streaming',
437
528
  assertion: 'settled.in-flight',
438
- evidence: { settled: false, inFlight, ...(streaming > 0 ? { streaming } : {}) },
529
+ evidence: {
530
+ settled: false,
531
+ inFlight,
532
+ ...(streaming > 0 ? { streaming } : {}),
533
+ ...disclosure,
534
+ },
439
535
  };
440
536
  }
441
537
  let lastT = -1;
@@ -449,12 +545,15 @@ export function evalSettled(events, p, now) {
449
545
  if (lastT < 0) {
450
546
  return {
451
547
  pass: true,
452
- evidence: { settled: true, quietForMs: null, note: 'no activity to settle' },
548
+ evidence: { settled: true, quietForMs: null, note: 'no activity to settle', ...disclosure },
453
549
  };
454
550
  }
455
551
  const quietForMs = now - lastT;
456
552
  if (quietForMs >= quietMs) {
457
- return { pass: true, evidence: { settled: true, quietForMs, lastActivity: lastType } };
553
+ return {
554
+ pass: true,
555
+ evidence: { settled: true, quietForMs, lastActivity: lastType, ...disclosure },
556
+ };
458
557
  }
459
558
  return {
460
559
  pass: false,
@@ -462,6 +561,11 @@ export function evalSettled(events, p, now) {
462
561
  observed: `last activity was ${String(lastType)}, ${String(quietForMs)}ms ago`,
463
562
  expected: `${String(quietMs)}ms of quiet`,
464
563
  assertion: 'settled.quiet',
465
- evidence: { quietForMs, lastActivity: lastType },
564
+ evidence: { quietForMs, lastActivity: lastType, ...disclosure },
565
+ // The one failure in this file that knows exactly when it could stop being one: if nothing else
566
+ // happens, the window closes in this many ms. A waiter that re-checks THEN instead of on its next
567
+ // blind tick stops paying up to a full poll interval on the hottest call in the product. Only a
568
+ // hint — the predicate is still evaluated at that moment, and can still fail.
569
+ retryAfterMs: quietMs - quietForMs,
466
570
  };
467
571
  }
@@ -1,8 +1,8 @@
1
- import { type CommandResult, type ReticleEvent } from '@reticlehq/core';
1
+ import { PredicateKind, type CommandResult, type ReticleEvent } from '@reticlehq/core';
2
2
  import type { ExpectedLink } from '../capsule/divergence.js';
3
3
  import { type AmbientCounts } from '../journal/ambient.js';
4
4
  import { PredicateSchema, type Predicate, type EvalResult } from './predicate-eval.js';
5
- export { PredicateSchema };
5
+ export { PredicateSchema, PredicateKind };
6
6
  export type { Predicate, EvalResult };
7
7
  /** The subset of Session the predicate engine needs — keeps it testable with a fake. */
8
8
  export interface PredicateSession {
@@ -1,9 +1,12 @@
1
- import { ElementState, ReticleCommand, } from '@reticlehq/core';
1
+ import { ElementState, PredicateKind, ReticleCommand, } from '@reticlehq/core';
2
+ import { log } from '../log.js';
3
+ import { bindSpanContext } from '../trace.js';
2
4
  import { selectPath, capDepth } from '../session/state-select.js';
5
+ import { describeTestidMiss } from './testid-near-miss.js';
3
6
  import { predicateToExpectedLinks } from '../capsule/predicate-to-links.js';
4
7
  import { isAmbient, ambientKeyOf } from '../journal/ambient.js';
5
8
  import { PredicateSchema, matchValue, evalNet, evalRoute, evalConsole, evalAnimation, evalSignal, evalSettled, } from './predicate-eval.js';
6
- export { PredicateSchema };
9
+ export { PredicateSchema, PredicateKind };
7
10
  async function matchOnce(session, query, state) {
8
11
  const res = await session.command(ReticleCommand.MATCH, { query, state });
9
12
  if (!res.ok)
@@ -23,7 +26,7 @@ async function evalElement(session, query, state, absent, diagnose) {
23
26
  // silently widening to the whole page is the original false green. So scopeMissing FAILS presence
24
27
  // (on the wait_for path this just keeps polling until the scope appears).
25
28
  if (absent) {
26
- if (match.scopeMissing === true) {
29
+ if (true === match.scopeMissing) {
27
30
  return { pass: true, evidence: { absent: true, scopeMissing: true } };
28
31
  }
29
32
  return match.matched
@@ -37,7 +40,7 @@ async function evalElement(session, query, state, absent, diagnose) {
37
40
  }
38
41
  : { pass: true, evidence: { absent: true } };
39
42
  }
40
- if (match.scopeMissing === true) {
43
+ if (true === match.scopeMissing) {
41
44
  return {
42
45
  pass: false,
43
46
  failureReason: `scope resolved to no element — cannot confirm ${subject} is present`,
@@ -96,12 +99,19 @@ async function evalElement(session, query, state, absent, diagnose) {
96
99
  };
97
100
  }
98
101
  }
102
+ // The testid near-miss: name what IS here, so a typo is one step from fixed rather than a dead
103
+ // end. reticle_query has always done this; the predicate path had no equivalent. See
104
+ // testid-near-miss.ts.
105
+ const present = match.hint?.presentTestids ?? [];
106
+ const alsoHere = query.testid === undefined ? undefined : describeTestidMiss(query.testid, present);
107
+ const suffix = alsoHere === undefined || '' === alsoHere ? '' : ` — ${alsoHere}`;
99
108
  return {
100
109
  pass: false,
101
- failureReason: `no element matched ${subject}${state === undefined ? '' : ` in state '${state}'`}`,
102
- observed: 'no matching element on the page',
110
+ failureReason: `no element matched ${subject}${state === undefined ? '' : ` in state '${state}'`}${suffix}`,
111
+ observed: `no matching element on the page${suffix}`,
103
112
  expected: `an element matching ${subject}${state === undefined ? '' : ` in state '${state}'`}`,
104
113
  assertion: 'element.present',
114
+ ...(present.length > 0 ? { evidence: { presentTestids: present } } : {}),
105
115
  };
106
116
  }
107
117
  async function evalState(session, p) {
@@ -117,14 +127,15 @@ async function evalState(session, p) {
117
127
  }
118
128
  const stores = (res.result ?? {}).stores ?? {};
119
129
  const names = Object.keys(stores);
120
- const storeName = p.store ?? (names.length === 1 ? names[0] : undefined);
130
+ const storeName = p.store ?? (1 === names.length ? names[0] : undefined);
121
131
  if (storeName === undefined) {
122
- return {
123
- pass: false,
124
- failureReason: names.length === 0
125
- ? 'no registered store to read state from'
126
- : `multiple stores (${names.join(', ')}); name one with \`store\``,
127
- };
132
+ // Neither of these is a finding about the app. With several stores registered the call simply did
133
+ // not say which one to read; with none, there is nothing to read at all. No assertion was
134
+ // evaluated either way, so this is inconclusive rather than failed — see honesty/inconclusive.
135
+ const reason = 0 === names.length
136
+ ? 'no registered store to read state from'
137
+ : `multiple stores (${names.join(', ')}); name one with \`store\``;
138
+ return { pass: false, failureReason: reason, inconclusive: reason };
128
139
  }
129
140
  const selection = selectPath(stores[storeName], p.path);
130
141
  if (!selection.found) {
@@ -153,31 +164,39 @@ async function evalState(session, p) {
153
164
  evidence: { store: storeName, path: p.path, value: capDepth(selection.value, 1) },
154
165
  };
155
166
  }
167
+ /** The predicate's own event-time floor, if its kind carries one. */
168
+ function predicateSince(predicate) {
169
+ return 'since' in predicate && 'number' === typeof predicate.since ? predicate.since : 0;
170
+ }
156
171
  export async function evaluatePredicate(session, predicate, since = 0,
157
172
  // Compute the (extra-round-trip) near-miss diagnostics on element failures. Default true so a
158
173
  // one-shot assert is fully diagnostic; the wait loop passes false on its interim polls (which read
159
174
  // only `pass`) and true on the final timeout eval, so a flood no longer pays for a diagnostic nobody
160
175
  // reads. Only element/text failures have a near-miss; everything else ignores this.
161
176
  diagnose = true) {
162
- const events = session.eventsSince(since);
177
+ // A predicate's own `since` is a TIGHTER event-time floor than the caller's — an agent that took
178
+ // `since` from the act it just performed is scoping the assertion to that action's aftermath. It is
179
+ // applied here, once, rather than in each eval: the floor means the same thing for every kind that
180
+ // reads the event stream, and net/console re-applying it is a no-op.
181
+ const events = session.eventsSince(Math.max(since, predicateSince(predicate)));
163
182
  switch (predicate.kind) {
164
- case 'element':
183
+ case PredicateKind.ELEMENT:
165
184
  return evalElement(session, predicate.query, predicate.state, predicate.absent ?? false, diagnose);
166
- case 'text':
167
- return evalElement(session, { text: predicate.contains }, predicate.visible === true ? ElementState.VISIBLE : undefined, predicate.absent ?? false, diagnose);
168
- case 'net':
185
+ case PredicateKind.TEXT:
186
+ return evalElement(session, { text: predicate.contains }, true === predicate.visible ? ElementState.VISIBLE : undefined, predicate.absent ?? false, diagnose);
187
+ case PredicateKind.NET:
169
188
  return evalNet(events, predicate);
170
- case 'route':
189
+ case PredicateKind.ROUTE:
171
190
  return evalRoute(events, predicate);
172
- case 'console':
191
+ case PredicateKind.CONSOLE:
173
192
  return evalConsole(events, predicate);
174
- case 'animation':
193
+ case PredicateKind.ANIMATION:
175
194
  return evalAnimation(events, predicate);
176
- case 'signal':
195
+ case PredicateKind.SIGNAL:
177
196
  return evalSignal(events, predicate);
178
- case 'state':
197
+ case PredicateKind.STATE:
179
198
  return evalState(session, predicate);
180
- case 'settled': {
199
+ case PredicateKind.SETTLED: {
181
200
  // Drop events on learned-ambient regions (chat/ticker churn) before the settle check — by ref
182
201
  // alone, NOT by attribution: window-attribution ("happened during the action window") is a time
183
202
  // heuristic, never causation, so a chat message arriving mid-window must not hold settle open.
@@ -185,7 +204,7 @@ diagnose = true) {
185
204
  const settleEvents = counts === undefined ? events : events.filter((e) => !isAmbient(counts, ambientKeyOf(e)));
186
205
  return evalSettled(settleEvents, predicate, session.elapsed());
187
206
  }
188
- case 'allOf': {
207
+ case PredicateKind.ALL_OF: {
189
208
  const results = await Promise.all(predicate.predicates.map((p) => evaluatePredicate(session, p, since, diagnose)));
190
209
  const failed = results.find((r) => !r.pass);
191
210
  return failed === undefined
@@ -196,14 +215,14 @@ diagnose = true) {
196
215
  evidence: results,
197
216
  };
198
217
  }
199
- case 'anyOf': {
218
+ case PredicateKind.ANY_OF: {
200
219
  const results = await Promise.all(predicate.predicates.map((p) => evaluatePredicate(session, p, since, diagnose)));
201
220
  const passed = results.find((r) => r.pass);
202
221
  return passed !== undefined
203
222
  ? { pass: true, evidence: passed.evidence }
204
223
  : { pass: false, failureReason: 'no sub-predicate of anyOf matched', evidence: results };
205
224
  }
206
- case 'not': {
225
+ case PredicateKind.NOT: {
207
226
  const inner = await evaluatePredicate(session, predicate.predicate, since, diagnose);
208
227
  return inner.pass
209
228
  ? { pass: false, failureReason: 'negated predicate unexpectedly held', evidence: inner }
@@ -244,12 +263,12 @@ const COUNT_CONFIRM_MS = 300;
244
263
  * must stay that way, or every ordinary wait pays the confirmation delay for nothing.
245
264
  */
246
265
  function assertsExactCount(predicate) {
247
- if (predicate.kind === 'allOf' || predicate.kind === 'anyOf') {
266
+ if (PredicateKind.ALL_OF === predicate.kind || PredicateKind.ANY_OF === predicate.kind) {
248
267
  return predicate.predicates.some(assertsExactCount);
249
268
  }
250
- if (predicate.kind === 'not')
269
+ if (PredicateKind.NOT === predicate.kind)
251
270
  return assertsExactCount(predicate.predicate);
252
- return predicate.kind === 'net' && predicate.count !== undefined;
271
+ return PredicateKind.NET === predicate.kind && predicate.count !== undefined;
253
272
  }
254
273
  /**
255
274
  * Evaluate now, else wait for it to become true (on each event + a poll) until timeout. `since` is
@@ -263,10 +282,24 @@ export function waitForPredicate(session, predicate, timeoutMs, since = 0) {
263
282
  failureReason: error instanceof Error ? error.message : String(error),
264
283
  });
265
284
  let cooldownTimer;
285
+ /** One-shot re-check timed to when a time-based predicate could first pass. See retryAfterMs. */
286
+ let hintTimer;
266
287
  // An exact-count wait keeps watching after it first reads true — see COUNT_CONFIRM_MS.
267
288
  const holdsForCount = assertsExactCount(predicate);
268
289
  let confirming = false;
269
290
  let confirmTimer;
291
+ /** Report a wait that could not run, and END it — see guardedCheck. */
292
+ const failWait = (error) => {
293
+ log('reticle_wait_failed', {
294
+ predicate: predicate.kind,
295
+ error: error instanceof Error ? error.message : String(error),
296
+ });
297
+ finish({
298
+ ...failed(error),
299
+ failureReason: 'the wait could not be evaluated and was ended rather than left pending: ' +
300
+ (error instanceof Error ? error.message : String(error)),
301
+ });
302
+ };
270
303
  const finish = (result) => {
271
304
  if (done)
272
305
  return;
@@ -276,6 +309,8 @@ export function waitForPredicate(session, predicate, timeoutMs, since = 0) {
276
309
  clearTimeout(timer);
277
310
  if (cooldownTimer !== undefined)
278
311
  clearTimeout(cooldownTimer);
312
+ if (hintTimer !== undefined)
313
+ clearTimeout(hintTimer);
279
314
  if (confirmTimer !== undefined)
280
315
  clearTimeout(confirmTimer);
281
316
  resolve(result);
@@ -305,8 +340,19 @@ export function waitForPredicate(session, predicate, timeoutMs, since = 0) {
305
340
  // final timeout eval below runs with full diagnostics.
306
341
  void evaluatePredicate(session, predicate, since, false)
307
342
  .then((r) => {
308
- if (!r.pass)
343
+ if (!r.pass) {
344
+ // A time-based failure knows when it could stop being one — re-check THEN rather than on
345
+ // the next blind tick. Without this, every `settled` wait paid up to a full poll interval
346
+ // of dead time after the quiet window had already closed: measured at 566–627ms across
347
+ // the fleet for a 500ms window, on the call an agent makes after almost every action.
348
+ // Additive — the backstop interval below still runs, so a missed hint costs nothing.
349
+ const hint = r.retryAfterMs;
350
+ if ('number' === typeof hint && hint > 0 && hint < POLL_INTERVAL_MS) {
351
+ clearTimeout(hintTimer);
352
+ hintTimer = setTimeout(check, hint);
353
+ }
309
354
  return;
355
+ }
310
356
  // "Exactly N" cannot be concluded from a passing sample — the count can still rise. Hold,
311
357
  // re-evaluate WITH diagnostics, and let that second read be the verdict: if an N+1th
312
358
  // arrived in the meantime it now fails, carrying observed/expected rather than a bare no.
@@ -344,10 +390,41 @@ export function waitForPredicate(session, predicate, timeoutMs, since = 0) {
344
390
  }, MIN_RECHECK_GAP_MS);
345
391
  });
346
392
  };
393
+ /**
394
+ * Run a re-check so that NOTHING can leave this promise pending.
395
+ *
396
+ * `check` is fired from an event listener and an interval, neither of which is inside the
397
+ * awaited chain. A synchronous throw there reached the process as an uncaughtException; a
398
+ * rejection reached it as an unhandledRejection. Either way the wait never resolved and the tool
399
+ * handler never returned — the agent sees a call that simply never comes back.
400
+ *
401
+ * That is the exact shape reported from a Plane (Next 14 + MobX) session, which tears the page
402
+ * session down and rebuilds it on EVERY navigation, so a `{kind:"route"}` predicate always races
403
+ * a teardown of the very session it is watching: `browser.command ok:true` for the click, and
404
+ * then no `tool.handler` for that callId, ever.
405
+ *
406
+ * A wait that cannot evaluate is a FAILED wait, not an eternal one. It now says so and finishes.
407
+ */
408
+ const guardedCheck = () => {
409
+ try {
410
+ const pending = check();
411
+ if (pending instanceof Promise)
412
+ pending.catch(failWait);
413
+ }
414
+ catch (error) {
415
+ failWait(error);
416
+ }
417
+ };
418
+ // Bound to the CALL that started this wait. The listener fires from the WebSocket message
419
+ // handler and the interval from the timer queue — neither is in the awaited chain, so without
420
+ // this every re-check opened a new call at depth 0 and emitted a `browser.command` with no
421
+ // `tool.handler`. Measured on one healthy run: 23 such orphans, which is precisely the
422
+ // documented signature of a HUNG call. A diagnostic that fires on healthy runs is not one.
423
+ const boundCheck = bindSpanContext(guardedCheck);
347
424
  const unsub = session.onEvent(() => {
348
- check();
425
+ boundCheck();
349
426
  });
350
- const interval = setInterval(check, POLL_INTERVAL_MS);
427
+ const interval = setInterval(boundCheck, POLL_INTERVAL_MS);
351
428
  const timer = setTimeout(() => {
352
429
  void evaluatePredicate(session, predicate, since)
353
430
  .then((r) => {
@@ -382,11 +459,11 @@ export function waitForPredicate(session, predicate, timeoutMs, since = 0) {
382
459
  * green ⇏ this-branch-held, re-checks each branch and keeps the winners.
383
460
  */
384
461
  export async function provenExpectedLinks(session, predicate, since = 0) {
385
- if (predicate.kind === 'allOf') {
462
+ if (PredicateKind.ALL_OF === predicate.kind) {
386
463
  const per = await Promise.all(predicate.predicates.map((p) => provenExpectedLinks(session, p, since)));
387
464
  return per.flat();
388
465
  }
389
- if (predicate.kind === 'anyOf') {
466
+ if (PredicateKind.ANY_OF === predicate.kind) {
390
467
  const per = await Promise.all(predicate.predicates.map(async (p) => (await evaluatePredicate(session, p, since)).pass
391
468
  ? provenExpectedLinks(session, p, since)
392
469
  : []));