@reticlehq/server 2.4.0 → 2.6.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 (472) hide show
  1. package/SKILL.md +719 -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} +13 -1
  7. package/dist/{bridge.js → bridge/bridge.js} +95 -15
  8. package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +9 -11
  9. package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +18 -10
  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/cli-doctor.d.ts +1 -0
  20. package/dist/cli/cli-doctor.js +135 -0
  21. package/dist/{cli-flow-commands.d.ts → cli/cli-flow-commands.d.ts} +2 -2
  22. package/dist/{cli-flow-commands.js → cli/cli-flow-commands.js} +15 -15
  23. package/dist/{cli-launch.d.ts → cli/cli-launch.d.ts} +32 -3
  24. package/dist/cli/cli-launch.js +171 -0
  25. package/dist/cli/cli-parse-feedback.d.ts +31 -0
  26. package/dist/cli/cli-parse-feedback.js +56 -0
  27. package/dist/{cli-parse.d.ts → cli/cli-parse.d.ts} +6 -19
  28. package/dist/{cli-parse.js → cli/cli-parse.js} +118 -81
  29. package/dist/{cli-port.d.ts → cli/cli-port.d.ts} +12 -0
  30. package/dist/{cli-port.js → cli/cli-port.js} +9 -9
  31. package/dist/cli/cli-update-commands.d.ts +4 -0
  32. package/dist/cli/cli-update-commands.js +93 -0
  33. package/dist/{cli-verify.js → cli/cli-verify.js} +14 -14
  34. package/dist/{cloud-cli.js → cli/cloud-cli.js} +29 -24
  35. package/dist/cli/doctor-daemon-line.d.ts +37 -0
  36. package/dist/cli/doctor-daemon-line.js +35 -0
  37. package/dist/cli/port-holder.d.ts +43 -0
  38. package/dist/cli/port-holder.js +99 -0
  39. package/dist/cli.d.ts +2 -2
  40. package/dist/cli.js +302 -100
  41. package/dist/cloud/cloud-config.d.ts +0 -1
  42. package/dist/cloud/cloud-config.js +9 -9
  43. package/dist/cloud/cloud-sync.d.ts +1 -7
  44. package/dist/cloud/cloud-sync.js +12 -12
  45. package/dist/crawl/crawl-empty.d.ts +16 -0
  46. package/dist/crawl/crawl-empty.js +24 -0
  47. package/dist/crawl/crawl-tools.js +4 -1
  48. package/dist/crawl/crawl.js +26 -7
  49. package/dist/daemon/daemon-resilience.d.ts +73 -0
  50. package/dist/daemon/daemon-resilience.js +279 -0
  51. package/dist/daemon/daemon-usefulness.d.ts +61 -0
  52. package/dist/daemon/daemon-usefulness.js +62 -0
  53. package/dist/{daemon.d.ts → daemon/daemon.d.ts} +33 -8
  54. package/dist/{daemon.js → daemon/daemon.js} +105 -48
  55. package/dist/daemon/heartbeat.d.ts +115 -0
  56. package/dist/daemon/heartbeat.js +141 -0
  57. package/dist/daemon/idle-grace.d.ts +32 -0
  58. package/dist/daemon/idle-grace.js +41 -0
  59. package/dist/{idle-shutdown.d.ts → daemon/idle-shutdown.d.ts} +23 -2
  60. package/dist/{idle-shutdown.js → daemon/idle-shutdown.js} +37 -6
  61. package/dist/daemon/port-presence.d.ts +76 -0
  62. package/dist/daemon/port-presence.js +93 -0
  63. package/dist/daemon/state-dir.d.ts +7 -0
  64. package/dist/daemon/state-dir.js +59 -0
  65. package/dist/daemon/unclean-predecessor.d.ts +23 -0
  66. package/dist/daemon/unclean-predecessor.js +27 -0
  67. package/dist/domain/domain-model.js +2 -2
  68. package/dist/events/body-failures.js +8 -8
  69. package/dist/events/contradictions.d.ts +1 -1
  70. package/dist/events/contradictions.js +60 -10
  71. package/dist/events/echo-mismatch.js +10 -10
  72. package/dist/events/event-filters.js +6 -6
  73. package/dist/events/json-salvage.d.ts +2 -1
  74. package/dist/events/json-salvage.js +6 -6
  75. package/dist/events/observed-in-window.d.ts +12 -0
  76. package/dist/events/observed-in-window.js +21 -0
  77. package/dist/events/predicate-eval.d.ts +53 -20
  78. package/dist/events/predicate-eval.js +179 -55
  79. package/dist/events/predicate-parse.d.ts +23 -0
  80. package/dist/events/predicate-parse.js +54 -0
  81. package/dist/events/predicate.d.ts +7 -2
  82. package/dist/events/predicate.js +126 -35
  83. package/dist/events/reconcile.js +5 -5
  84. package/dist/events/ring-buffer.js +2 -17
  85. package/dist/events/stale-response.js +6 -6
  86. package/dist/events/testid-near-miss.d.ts +22 -0
  87. package/dist/events/testid-near-miss.js +33 -0
  88. package/dist/events/unit-mismatch.js +8 -8
  89. package/dist/flows/affected-tools.js +19 -4
  90. package/dist/flows/affected.js +1 -1
  91. package/dist/flows/annotate-target.d.ts +1 -0
  92. package/dist/flows/annotate-target.js +20 -0
  93. package/dist/flows/annotate-tools.js +25 -3
  94. package/dist/flows/annotate.js +17 -4
  95. package/dist/flows/assertion-integrity.d.ts +2 -1
  96. package/dist/flows/assertion-tiers-store.d.ts +3 -66
  97. package/dist/flows/assertion-tiers-store.js +2 -2
  98. package/dist/flows/attributed-failure.d.ts +17 -0
  99. package/dist/flows/attributed-failure.js +20 -0
  100. package/dist/flows/buddy-status.d.ts +2 -1
  101. package/dist/flows/buddy-status.js +2 -2
  102. package/dist/flows/coverage.d.ts +3 -2
  103. package/dist/flows/coverage.js +2 -2
  104. package/dist/flows/decision.js +24 -9
  105. package/dist/flows/empty-flow.d.ts +6 -0
  106. package/dist/flows/empty-flow.js +24 -0
  107. package/dist/flows/flake-store.d.ts +0 -1
  108. package/dist/flows/flake-store.js +1 -1
  109. package/dist/flows/flake.d.ts +2 -1
  110. package/dist/flows/flake.js +2 -2
  111. package/dist/flows/flow-classify.js +12 -0
  112. package/dist/flows/flow-replay-run.js +4 -4
  113. package/dist/flows/flow-replay.d.ts +6 -8
  114. package/dist/flows/flow-replay.js +132 -48
  115. package/dist/flows/flow-report.js +1 -1
  116. package/dist/flows/flow-scope.d.ts +1 -6
  117. package/dist/flows/flow-step-runners.d.ts +26 -6
  118. package/dist/flows/flow-step-runners.js +156 -6
  119. package/dist/flows/flow-success.js +20 -15
  120. package/dist/flows/flow-tools.js +63 -21
  121. package/dist/flows/flows.js +15 -11
  122. package/dist/flows/gate.d.ts +4 -3
  123. package/dist/flows/gate.js +1 -1
  124. package/dist/flows/heal-run.d.ts +0 -8
  125. package/dist/flows/heal-run.js +5 -5
  126. package/dist/flows/heal.js +3 -3
  127. package/dist/flows/predicate-to-expect.d.ts +38 -0
  128. package/dist/flows/predicate-to-expect.js +95 -0
  129. package/dist/flows/replay.d.ts +16 -1
  130. package/dist/flows/replay.js +58 -22
  131. package/dist/flows/role-anchor-nearest.d.ts +6 -0
  132. package/dist/flows/role-anchor-nearest.js +45 -0
  133. package/dist/flows/role-drift-reason.d.ts +20 -0
  134. package/dist/flows/role-drift-reason.js +39 -0
  135. package/dist/flows/server-verify.d.ts +1 -1
  136. package/dist/flows/server-verify.js +5 -5
  137. package/dist/flows/suite-flakes.d.ts +33 -0
  138. package/dist/flows/suite-flakes.js +32 -0
  139. package/dist/flows/verify-change-tools.js +32 -10
  140. package/dist/flows/verify-change-verdict.d.ts +15 -0
  141. package/dist/flows/verify-change-verdict.js +25 -0
  142. package/dist/flows/watch-batcher.d.ts +3 -2
  143. package/dist/honesty/already-true.d.ts +2 -0
  144. package/dist/honesty/already-true.js +31 -0
  145. package/dist/honesty/blind-spots.d.ts +35 -3
  146. package/dist/honesty/blind-spots.js +57 -12
  147. package/dist/honesty/honesty.d.ts +3 -2
  148. package/dist/honesty/honesty.js +4 -4
  149. package/dist/honesty/uncaptured-bodies.js +1 -1
  150. package/dist/honesty/unread-outcome.js +2 -2
  151. package/dist/honesty/verified.d.ts +31 -3
  152. package/dist/honesty/verified.js +64 -8
  153. package/dist/http-server.js +23 -7
  154. package/dist/hunt/hunt-report.d.ts +2 -1
  155. package/dist/hunt/hunt-report.js +2 -2
  156. package/dist/index.d.ts +9 -6
  157. package/dist/index.js +41 -25
  158. package/dist/init/agent-rules.d.ts +20 -1
  159. package/dist/init/agent-rules.js +64 -8
  160. package/dist/init/app-choice.d.ts +20 -0
  161. package/dist/init/app-choice.js +25 -0
  162. package/dist/init/astro-host.d.ts +21 -0
  163. package/dist/init/astro-host.js +27 -0
  164. package/dist/init/astro-patch.d.ts +16 -0
  165. package/dist/init/astro-patch.js +240 -0
  166. package/dist/init/capabilities.js +4 -1
  167. package/dist/init/cra.d.ts +50 -0
  168. package/dist/init/cra.js +106 -0
  169. package/dist/init/cursor.js +39 -4
  170. package/dist/init/desktop-doctor.d.ts +3 -2
  171. package/dist/init/desktop-doctor.js +4 -4
  172. package/dist/init/detect.d.ts +9 -2
  173. package/dist/init/detect.js +24 -5
  174. package/dist/init/mcp-clients.d.ts +81 -0
  175. package/dist/init/mcp-clients.js +248 -0
  176. package/dist/init/mcp-registered.d.ts +12 -0
  177. package/dist/init/mcp-registered.js +14 -0
  178. package/dist/init/mcp.d.ts +27 -2
  179. package/dist/init/mcp.js +40 -4
  180. package/dist/init/next-patch.d.ts +2 -4
  181. package/dist/init/next-patch.js +109 -19
  182. package/dist/init/node-io.js +29 -6
  183. package/dist/init/plan-framework.d.ts +16 -1
  184. package/dist/init/plan-framework.js +166 -8
  185. package/dist/init/plan.d.ts +85 -6
  186. package/dist/init/plan.js +196 -33
  187. package/dist/init/project-id.d.ts +5 -9
  188. package/dist/init/project-id.js +10 -22
  189. package/dist/init/run.d.ts +23 -1
  190. package/dist/init/run.js +338 -47
  191. package/dist/init/slash-command.d.ts +9 -0
  192. package/dist/init/slash-command.js +10 -1
  193. package/dist/init/snippets.d.ts +7 -10
  194. package/dist/init/snippets.js +44 -15
  195. package/dist/init/vite-config.js +1 -1
  196. package/dist/init/windows-quote.d.ts +38 -0
  197. package/dist/init/windows-quote.js +70 -0
  198. package/dist/init/workspace-apps.d.ts +23 -0
  199. package/dist/init/workspace-apps.js +68 -0
  200. package/dist/input/driven-redaction.js +1 -1
  201. package/dist/input/network-detail.js +8 -8
  202. package/dist/input/network-mock-tools.js +2 -2
  203. package/dist/input/network-mock.js +4 -4
  204. package/dist/input/real-input.js +8 -3
  205. package/dist/input/scroll-find.js +2 -2
  206. package/dist/input/scroll-tools.d.ts +0 -5
  207. package/dist/input/scroll-tools.js +11 -4
  208. package/dist/input/viewport-tools.js +2 -2
  209. package/dist/journal/ambient-store.d.ts +0 -2
  210. package/dist/journal/ambient-store.js +1 -1
  211. package/dist/journal/ambient.js +1 -1
  212. package/dist/journal/attach-journal.d.ts +2 -1
  213. package/dist/journal/deviation-report.js +6 -6
  214. package/dist/journal/deviation-service.js +1 -1
  215. package/dist/journal/envelope-store.d.ts +0 -2
  216. package/dist/journal/envelope-store.js +1 -1
  217. package/dist/journal/envelope.d.ts +2 -3
  218. package/dist/journal/envelope.js +3 -3
  219. package/dist/journal/journal-query.js +1 -1
  220. package/dist/journal/journal-recorder.d.ts +2 -1
  221. package/dist/journal/journal-recorder.js +2 -2
  222. package/dist/journal/rollups.js +9 -9
  223. package/dist/journal/session-end.d.ts +2 -1
  224. package/dist/journal/session-journal.js +4 -4
  225. package/dist/license/license.d.ts +0 -14
  226. package/dist/license/license.js +8 -8
  227. package/dist/log.d.ts +10 -0
  228. package/dist/log.js +11 -1
  229. package/dist/mcp/mcp-is-error.d.ts +13 -0
  230. package/dist/mcp/mcp-is-error.js +18 -0
  231. package/dist/mcp/mcp-outage.d.ts +32 -0
  232. package/dist/mcp/mcp-outage.js +55 -0
  233. package/dist/mcp/mcp-proxy.d.ts +134 -0
  234. package/dist/mcp/mcp-proxy.js +720 -0
  235. package/dist/{mcp.d.ts → mcp/mcp.d.ts} +13 -10
  236. package/dist/{mcp.js → mcp/mcp.js} +139 -37
  237. package/dist/mcp/proxy-handshake.d.ts +3 -0
  238. package/dist/mcp/proxy-handshake.js +54 -0
  239. package/dist/mcp/proxy-lifecycle.d.ts +64 -0
  240. package/dist/mcp/proxy-lifecycle.js +70 -0
  241. package/dist/mcp/tool-catalog-cache.d.ts +29 -0
  242. package/dist/mcp/tool-catalog-cache.js +55 -0
  243. package/dist/oracles/propose-consequences.d.ts +2 -1
  244. package/dist/oracles/propose-consequences.js +16 -11
  245. package/dist/oracles/self-instrument.d.ts +2 -1
  246. package/dist/oracles/self-instrument.js +1 -1
  247. package/dist/phenomena/phenomena.js +3 -3
  248. package/dist/platform.d.ts +16 -0
  249. package/dist/platform.js +16 -0
  250. package/dist/pool/browser-pool.d.ts +9 -0
  251. package/dist/pool/browser-pool.js +38 -9
  252. package/dist/pool/playwright-launcher.d.ts +15 -2
  253. package/dist/pool/playwright-launcher.js +21 -2
  254. package/dist/project/fs-port.js +1 -1
  255. package/dist/project/project-tools.js +2 -2
  256. package/dist/runs/build-verification-run.js +1 -1
  257. package/dist/runs/repair-prompt.d.ts +0 -6
  258. package/dist/runs/repair-prompt.js +3 -3
  259. package/dist/runs/risk-classify.js +1 -1
  260. package/dist/runs/run-diff.d.ts +0 -2
  261. package/dist/runs/run-diff.js +3 -3
  262. package/dist/runs/run-tools.js +2 -2
  263. package/dist/runs/verification-sync.js +2 -2
  264. package/dist/runs/verify-http.js +1 -1
  265. package/dist/runs/verify-server.js +1 -1
  266. package/dist/session/attachment-history.d.ts +46 -0
  267. package/dist/session/attachment-history.js +65 -0
  268. package/dist/session/command-timeout.d.ts +11 -3
  269. package/dist/session/command-timeout.js +2 -2
  270. package/dist/session/control-envelope.d.ts +6 -0
  271. package/dist/session/control-envelope.js +7 -1
  272. package/dist/session/dev-server-probe.d.ts +13 -0
  273. package/dist/session/dev-server-probe.js +52 -0
  274. package/dist/session/last-act.d.ts +17 -3
  275. package/dist/session/last-act.js +16 -4
  276. package/dist/session/live-control-tools.js +7 -2
  277. package/dist/session/live-control.js +1 -1
  278. package/dist/session/looks-like-dev-server.d.ts +14 -0
  279. package/dist/session/looks-like-dev-server.js +25 -0
  280. package/dist/session/no-session-diagnosis.d.ts +45 -0
  281. package/dist/session/no-session-diagnosis.js +120 -0
  282. package/dist/session/no-session-watch.d.ts +21 -0
  283. package/dist/session/no-session-watch.js +69 -0
  284. package/dist/session/observed-state.d.ts +4 -0
  285. package/dist/session/observed-state.js +19 -1
  286. package/dist/session/output-budget.js +1 -1
  287. package/dist/session/pending-commands.js +1 -0
  288. package/dist/session/session-health.d.ts +8 -1
  289. package/dist/session/session-health.js +8 -7
  290. package/dist/session/session-info.d.ts +43 -8
  291. package/dist/session/session-info.js +46 -1
  292. package/dist/session/session-lease.d.ts +0 -11
  293. package/dist/session/session-lease.js +1 -1
  294. package/dist/session/session-manager.d.ts +6 -0
  295. package/dist/session/session-manager.js +124 -12
  296. package/dist/session/session-readiness.d.ts +1 -1
  297. package/dist/session/session-readiness.js +6 -1
  298. package/dist/session/session-reaper.js +1 -1
  299. package/dist/session/session-reconnect.d.ts +32 -0
  300. package/dist/session/session-reconnect.js +42 -0
  301. package/dist/session/session.d.ts +15 -2
  302. package/dist/session/session.js +77 -26
  303. package/dist/session/state-select.d.ts +1 -1
  304. package/dist/session/state-select.js +1 -1
  305. package/dist/status-payload.d.ts +19 -0
  306. package/dist/status-payload.js +11 -0
  307. package/dist/telemetry/argument-shape.js +10 -1
  308. package/dist/telemetry/browser-mode.d.ts +27 -0
  309. package/dist/telemetry/browser-mode.js +33 -0
  310. package/dist/telemetry/bug-found.d.ts +17 -21
  311. package/dist/telemetry/bug-found.js +120 -8
  312. package/dist/telemetry/cli-telemetry.d.ts +2 -12
  313. package/dist/telemetry/cli-telemetry.js +27 -4
  314. package/dist/telemetry/crash-cause.d.ts +47 -0
  315. package/dist/telemetry/crash-cause.js +104 -0
  316. package/dist/telemetry/daemon-telemetry.d.ts +38 -1
  317. package/dist/telemetry/daemon-telemetry.js +37 -12
  318. package/dist/telemetry/dev-repo.js +4 -4
  319. package/dist/telemetry/error-class.d.ts +26 -0
  320. package/dist/telemetry/error-class.js +52 -0
  321. package/dist/telemetry/error-fingerprint.js +21 -2
  322. package/dist/telemetry/feedback-cli.d.ts +8 -3
  323. package/dist/telemetry/feedback-cli.js +52 -8
  324. package/dist/telemetry/feedback-context.d.ts +23 -6
  325. package/dist/telemetry/feedback-context.js +105 -5
  326. package/dist/telemetry/feedback-delivery.d.ts +20 -0
  327. package/dist/telemetry/feedback-delivery.js +30 -0
  328. package/dist/telemetry/feedback-outbox.d.ts +22 -0
  329. package/dist/telemetry/feedback-outbox.js +87 -0
  330. package/dist/telemetry/feedback.d.ts +22 -1
  331. package/dist/telemetry/feedback.js +67 -8
  332. package/dist/telemetry/git-facts.d.ts +7 -0
  333. package/dist/telemetry/git-facts.js +5 -5
  334. package/dist/telemetry/project-profile.js +2 -1
  335. package/dist/telemetry/session-metrics.d.ts +47 -3
  336. package/dist/telemetry/session-metrics.js +315 -15
  337. package/dist/telemetry/telemetry.d.ts +45 -2
  338. package/dist/telemetry/telemetry.js +150 -37
  339. package/dist/telemetry/verification-of.d.ts +22 -0
  340. package/dist/telemetry/verification-of.js +84 -0
  341. package/dist/tools/act-capsule.js +2 -2
  342. package/dist/tools/act-danger.d.ts +12 -0
  343. package/dist/tools/act-danger.js +20 -3
  344. package/dist/tools/act-helpers.js +4 -4
  345. package/dist/tools/act-tools.d.ts +0 -8
  346. package/dist/tools/act-tools.js +114 -118
  347. package/dist/tools/act-view.js +7 -7
  348. package/dist/tools/alias-args.d.ts +17 -0
  349. package/dist/tools/alias-args.js +25 -0
  350. package/dist/tools/assert-grade.js +9 -9
  351. package/dist/tools/assert-verdict.d.ts +9 -1
  352. package/dist/tools/assert-verdict.js +21 -6
  353. package/dist/tools/browser-tools.js +36 -11
  354. package/dist/tools/contract-tools.d.ts +0 -5
  355. package/dist/tools/contract-tools.js +27 -1
  356. package/dist/tools/coverage-identity.d.ts +34 -0
  357. package/dist/tools/coverage-identity.js +46 -0
  358. package/dist/tools/coverage-tools.js +10 -14
  359. package/dist/tools/dynamic-tools.d.ts +2 -1
  360. package/dist/tools/dynamic-tools.js +96 -19
  361. package/dist/tools/error-recovery.d.ts +34 -2
  362. package/dist/tools/error-recovery.js +255 -4
  363. package/dist/tools/feedback-invite.d.ts +48 -0
  364. package/dist/tools/feedback-invite.js +71 -0
  365. package/dist/tools/feedback-tools.d.ts +12 -1
  366. package/dist/tools/feedback-tools.js +28 -5
  367. package/dist/tools/invoke-tool.d.ts +0 -12
  368. package/dist/tools/invoke-tool.js +126 -33
  369. package/dist/tools/lease-hint.d.ts +17 -0
  370. package/dist/tools/lease-hint.js +24 -0
  371. package/dist/tools/lease-tools.js +6 -5
  372. package/dist/tools/merge-tools.d.ts +10 -1
  373. package/dist/tools/merge-tools.js +9 -3
  374. package/dist/tools/merged-name-redirect.d.ts +13 -0
  375. package/dist/tools/merged-name-redirect.js +55 -0
  376. package/dist/tools/navigate-arrival.d.ts +26 -0
  377. package/dist/tools/navigate-arrival.js +60 -0
  378. package/dist/tools/navigate-result.d.ts +31 -0
  379. package/dist/tools/navigate-result.js +38 -0
  380. package/dist/tools/observe-tools.js +72 -20
  381. package/dist/tools/observed-nothing.d.ts +25 -0
  382. package/dist/tools/observed-nothing.js +33 -0
  383. package/dist/tools/query-paginate.js +2 -2
  384. package/dist/tools/query-shape.d.ts +18 -0
  385. package/dist/tools/query-shape.js +39 -0
  386. package/dist/tools/read-tools.js +81 -20
  387. package/dist/tools/real-input-attempt.d.ts +28 -0
  388. package/dist/tools/real-input-attempt.js +93 -0
  389. package/dist/tools/reconcile-tools.js +7 -7
  390. package/dist/tools/reload-result.d.ts +14 -0
  391. package/dist/tools/reload-result.js +29 -0
  392. package/dist/tools/snapshot-delta.d.ts +4 -5
  393. package/dist/tools/snapshot-delta.js +0 -0
  394. package/dist/tools/tool-kit.d.ts +16 -0
  395. package/dist/tools/tool-surface.d.ts +83 -0
  396. package/dist/tools/tool-surface.js +217 -0
  397. package/dist/tools/tools-helpers.js +6 -6
  398. package/dist/tools/tools.d.ts +15 -0
  399. package/dist/tools/tools.js +53 -17
  400. package/dist/tools/unadvertised-help.d.ts +5 -0
  401. package/dist/tools/unadvertised-help.js +39 -0
  402. package/dist/trace.d.ts +69 -0
  403. package/dist/trace.js +150 -0
  404. package/dist/update/nudge-credit.d.ts +4 -0
  405. package/dist/update/nudge-credit.js +53 -0
  406. package/dist/update/sdk-sync.d.ts +21 -0
  407. package/dist/update/sdk-sync.js +42 -0
  408. package/dist/update/update-checker.js +3 -3
  409. package/dist/update/update-nudge.d.ts +63 -6
  410. package/dist/update/update-nudge.js +128 -15
  411. package/dist/update/updater.d.ts +1 -9
  412. package/dist/update/updater.js +13 -8
  413. package/dist/version/peer-announce.d.ts +4 -0
  414. package/dist/version/peer-announce.js +30 -0
  415. package/dist/{server-version.js → version/server-version.js} +5 -1
  416. package/dist/version/version-nudge.d.ts +28 -0
  417. package/dist/version/version-nudge.js +40 -0
  418. package/dist/version/version-skew.d.ts +66 -0
  419. package/dist/version/version-skew.js +71 -0
  420. package/dist/visual/visual-diff.js +2 -2
  421. package/dist/visual/visual-tools.js +4 -4
  422. package/docs/README.md +55 -0
  423. package/docs/agent-cheatsheet.md +85 -0
  424. package/docs/architecture.md +113 -0
  425. package/docs/benchmarks.md +255 -0
  426. package/docs/debugging.md +102 -0
  427. package/docs/deploy-checks.md +69 -0
  428. package/docs/desktop-apps.md +256 -0
  429. package/docs/enterprise.md +62 -0
  430. package/docs/fixtures-dispatch-receiver.yml +76 -0
  431. package/docs/fixtures.md +45 -0
  432. package/docs/flows.md +166 -0
  433. package/docs/gate-plan.md +98 -0
  434. package/docs/gates.md +117 -0
  435. package/docs/getting-started.md +469 -0
  436. package/docs/human-control.md +63 -0
  437. package/docs/integration-patterns.md +171 -0
  438. package/docs/local-registry.md +126 -0
  439. package/docs/matrix/2.5.0/claude-code-machine.json +19 -0
  440. package/docs/matrix/2.5.0/codex-machine.json +20 -0
  441. package/docs/matrix/2.5.0/cursor-machine.json +22 -0
  442. package/docs/matrix/2.5.0/gemini-machine.json +22 -0
  443. package/docs/matrix/2.5.0/opencode-machine.json +22 -0
  444. package/docs/matrix/2.5.0/vscode-machine.json +22 -0
  445. package/docs/matrix/2.5.0/windsurf-machine.json +22 -0
  446. package/docs/matrix/MATRIX.md +26 -0
  447. package/docs/matrix/README.md +59 -0
  448. package/docs/mint.json +61 -0
  449. package/docs/multi-agent-testing.md +55 -0
  450. package/docs/platform-integration.md +122 -0
  451. package/docs/system-map.md +152 -0
  452. package/docs/telemetry-contract.md +214 -0
  453. package/docs/telemetry.md +191 -0
  454. package/docs/testing.md +82 -0
  455. package/docs/token-efficiency.md +80 -0
  456. package/docs/usage.md +1046 -0
  457. package/package.json +8 -5
  458. package/dist/cli-doctor.d.ts +0 -6
  459. package/dist/cli-doctor.js +0 -60
  460. package/dist/cli-launch.js +0 -112
  461. package/dist/daemon-resilience.d.ts +0 -22
  462. package/dist/daemon-resilience.js +0 -85
  463. package/dist/mcp-proxy.d.ts +0 -67
  464. package/dist/mcp-proxy.js +0 -337
  465. package/dist/tools/profiles.d.ts +0 -37
  466. package/dist/tools/profiles.js +0 -139
  467. /package/dist/{pairing-token.d.ts → bridge/pairing-token.d.ts} +0 -0
  468. /package/dist/{token-auth.d.ts → bridge/token-auth.d.ts} +0 -0
  469. /package/dist/{token-auth.js → bridge/token-auth.js} +0 -0
  470. /package/dist/{cli-verify.d.ts → cli/cli-verify.d.ts} +0 -0
  471. /package/dist/{cloud-cli.d.ts → cli/cloud-cli.d.ts} +0 -0
  472. /package/dist/{server-version.d.ts → version/server-version.d.ts} +0 -0
@@ -1,7 +1,9 @@
1
- import { AnchorKind, DriftReason, EventType, ReticleCommand, QueryBy, } from '@reticlehq/core';
1
+ import { span } from '../trace.js';
2
+ import { AnchorKind, DriftReason, EventType, ReticleCommand, QueryBy, PredicateKind, } from '@reticlehq/core';
2
3
  import { asRecord, asString } from '../tools/tools-helpers.js';
3
4
  import { replayActionArgs, ambiguousTestidNote, queryRefs } from './replay.js';
4
- import { runRoleStep, runComponentStep } from './flow-step-runners.js';
5
+ import { degradedStepResult, isDegradedAnchor, runComponentStep, runRoleStep, runSequenceStep, } from './flow-step-runners.js';
6
+ import { successToPredicate } from './flow-success.js';
5
7
  import { ReticleTool } from '../tools/tool-names.js';
6
8
  /**
7
9
  * A single ASCII-ish edit distance (case-insensitive). Small inputs (testids), so O(n*m) is fine.
@@ -59,6 +61,22 @@ export function nearestTestid(missing, present) {
59
61
  */
60
62
  const ANCHOR_SETTLE_ATTEMPTS = 8;
61
63
  const ANCHOR_SETTLE_DELAY_MS = 150;
64
+ /**
65
+ * The budget that actually governs, in wall-clock milliseconds.
66
+ *
67
+ * Attempts were the bound, and on an event-chatty page that collapsed. `settleTick` ends on EITHER
68
+ * an event OR the tick, so a page emitting continuously (API calls, a large form render, CSS
69
+ * transition start/end pairs) burned all eight attempts in **224–758ms measured** — a budget
70
+ * documented as 1.2s, spent in a fifth of it, before a newly routed page had mounted its controls.
71
+ * Cross-route replays drifted `testid_not_found` at 278ms while the same flow passed on a quiet page.
72
+ *
73
+ * An event arriving is evidence the page is still working. It should EXTEND the wait, not spend it.
74
+ * So the deadline decides when to give up, and the attempt cap below survives only as a backstop
75
+ * against a pathological storm spinning this loop hot.
76
+ */
77
+ const ANCHOR_SETTLE_BUDGET_MS = ANCHOR_SETTLE_ATTEMPTS * ANCHOR_SETTLE_DELAY_MS;
78
+ /** Backstop only. Generous on purpose: the deadline is the real bound, this just prevents a spin. */
79
+ const ANCHOR_SETTLE_MAX_ATTEMPTS = 40;
62
80
  const realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
63
81
  /** Extract the live element refs + the zero-match near-miss hint from a QUERY command result. */
64
82
  function readQuery(result) {
@@ -67,10 +85,10 @@ function readQuery(result) {
67
85
  return { refs };
68
86
  const payload = asRecord(result.result);
69
87
  const rawHint = payload['hint'];
70
- if (typeof rawHint === 'object' && rawHint !== null) {
88
+ if ('object' === typeof rawHint && rawHint !== null) {
71
89
  const hint = asRecord(rawHint);
72
90
  const present = Array.isArray(hint['presentTestids'])
73
- ? hint['presentTestids'].filter((t) => typeof t === 'string')
91
+ ? hint['presentTestids'].filter((t) => 'string' === typeof t)
74
92
  : [];
75
93
  return {
76
94
  refs,
@@ -78,7 +96,7 @@ function readQuery(result) {
78
96
  route: asString(hint['route']) ?? '',
79
97
  presentTestids: present,
80
98
  presentRegions: [],
81
- knownEmptyState: hint['knownEmptyState'] === true,
99
+ knownEmptyState: true === hint['knownEmptyState'],
82
100
  },
83
101
  };
84
102
  }
@@ -107,7 +125,7 @@ export function nearestIsAmbiguous(missing, present) {
107
125
  return count >= 2;
108
126
  }
109
127
  /** Build the legible-drift record for a testid anchor that resolved to zero live elements. */
110
- function testidDrift(value, hint) {
128
+ export function testidDrift(value, hint) {
111
129
  const present = hint?.presentTestids ?? [];
112
130
  const drift = {
113
131
  reasonKind: DriftReason.TESTID_NOT_FOUND,
@@ -125,10 +143,42 @@ function testidDrift(value, hint) {
125
143
  * so a present anchor costs one query; a genuinely missing one costs the full (bounded) settle and
126
144
  * then drifts. The last result's near-miss hint is returned for the drift record.
127
145
  */
128
- export async function resolveQuery(session, queryArgs, sleep) {
146
+ /**
147
+ * One wait between anchor attempts: the fixed tick, or the next DOM event — whichever comes first.
148
+ *
149
+ * An element mounting IS a mutation, and the session already streams those, so sleeping out the rest
150
+ * of a 150ms tick after the thing has already appeared is pure latency. Measured on next-app-router:
151
+ * a single `flow.step` span was 1079ms around nine QUERY round-trips of 1–2ms each — the cost was
152
+ * entirely the sleeping, and four such steps made up 4.3s of that app's 7.6s.
153
+ *
154
+ * It can only resolve the wait EARLIER, never end the loop earlier: the attempt budget above is
155
+ * untouched, so a genuinely missing anchor still spends the full settle before it drifts. An early
156
+ * "not found" would be a false drift, which is the failure this loop exists to prevent.
157
+ */
158
+ async function settleTick(session, sleep) {
159
+ let unsubscribe;
160
+ try {
161
+ await new Promise((resolve) => {
162
+ let done = false;
163
+ const finish = () => {
164
+ if (done)
165
+ return;
166
+ done = true;
167
+ resolve();
168
+ };
169
+ unsubscribe = session.onEvent(finish);
170
+ void sleep(ANCHOR_SETTLE_DELAY_MS).then(finish);
171
+ });
172
+ }
173
+ finally {
174
+ unsubscribe?.();
175
+ }
176
+ }
177
+ export async function resolveQuery(session, queryArgs, sleep, now = Date.now) {
129
178
  let last = readQuery(await session.command(ReticleCommand.QUERY, queryArgs));
130
- for (let attempt = 1; last.refs.length === 0 && attempt < ANCHOR_SETTLE_ATTEMPTS; attempt += 1) {
131
- await sleep(ANCHOR_SETTLE_DELAY_MS);
179
+ const deadline = now() + ANCHOR_SETTLE_BUDGET_MS;
180
+ for (let attempt = 1; 0 === last.refs.length && now() < deadline && attempt < ANCHOR_SETTLE_MAX_ATTEMPTS; attempt += 1) {
181
+ await settleTick(session, sleep);
132
182
  last = readQuery(await session.command(ReticleCommand.QUERY, queryArgs));
133
183
  }
134
184
  return last;
@@ -188,7 +238,7 @@ function summarizeConsequence(events) {
188
238
  const data = n.data ?? {};
189
239
  const method = asString(data['method']) ?? 'GET';
190
240
  const path = trimUrl(asString(data['url']) ?? '');
191
- const status = typeof data['status'] === 'number' ? ` ${data['status']}` : '';
241
+ const status = 'number' === typeof data['status'] ? ` ${data['status']}` : '';
192
242
  parts.push(`${method} ${path}${status}`.trim());
193
243
  }
194
244
  const errors = events.filter((e) => e.type === EventType.CONSOLE_ERROR || e.type === EventType.ERROR_UNCAUGHT).length;
@@ -206,7 +256,7 @@ export function componentLabel(anchor) {
206
256
  return anchor.component ?? anchor.name ?? anchor.role ?? 'component';
207
257
  }
208
258
  /** The value of a step's primary anchor, for labelling the result row. */
209
- function anchorLabel(anchor) {
259
+ export function anchorLabel(anchor) {
210
260
  if (anchor.kind === AnchorKind.TESTID)
211
261
  return anchor.value;
212
262
  if (anchor.kind === AnchorKind.SIGNAL)
@@ -227,7 +277,7 @@ export function componentQueryArgs(anchor) {
227
277
  /** Run one testid-anchored step: re-resolve via QUERY, then ACT on the live ref, else drift. */
228
278
  async function runTestidStep(session, step, index, value, dynamic, confirmDangerous, sleep) {
229
279
  const { refs, hint } = await resolveTestid(session, value, sleep);
230
- if (refs.length === 0) {
280
+ if (0 === refs.length) {
231
281
  return {
232
282
  step: index,
233
283
  tool: step.tool,
@@ -263,7 +313,7 @@ async function runTestidStep(session, step, index, value, dynamic, confirmDanger
263
313
  const expectTestid = step.expect?.element?.testid;
264
314
  if (expectTestid !== undefined && !dynamic.has(expectTestid)) {
265
315
  const expectRefs = await resolveTestid(session, expectTestid, sleep);
266
- if (expectRefs.refs.length === 0) {
316
+ if (0 === expectRefs.refs.length) {
267
317
  return {
268
318
  step: index,
269
319
  tool: step.tool,
@@ -278,27 +328,50 @@ async function runTestidStep(session, step, index, value, dynamic, confirmDanger
278
328
  return result;
279
329
  }
280
330
  /**
281
- * After a step's anchor resolves + its action runs, an `expect.state` additionally asserts STORE
282
- * TRUTH — the source of truth no DOM read can reach. Evaluated with the same predicate engine (waits
283
- * up to the timeout so an async store update can settle); a mismatch is legible drift, not a blind
284
- * fail. Returns the drift on mismatch, else undefined.
331
+ * After a step's anchor resolves and its action runs, its `expect` is EVALUATED — every kind of it.
332
+ *
333
+ * For a long time only `expect.state` was, so a recorded `expect.signal` or `expect.net` was written
334
+ * to disk and read by nothing while `flow_save` graded the flow "asserted". Driven end to end over
335
+ * MCP: annotate a step with a signal that never fires, save (grade "asserted"), replay -> status
336
+ * "ok". A green that cannot go red, inside the feature whose job is catching exactly that.
337
+ *
338
+ * It compiles through the SAME `successToPredicate` the flow-level `success` has always used, so
339
+ * there is one definition of what an expect means and the step form cannot drift from it again.
340
+ * Turning this on makes previously-green flows go red. That is the point — they were green because
341
+ * nothing was looking.
285
342
  */
286
- async function assertStepState(session, state, waitForSignal, timeoutMs, since) {
287
- const predicate = { kind: 'state', path: state.path };
288
- if (state.store !== undefined)
289
- predicate.store = state.store;
290
- if (state.equals !== undefined)
291
- predicate.equals = state.equals;
343
+ async function assertStepExpect(session, expect, dynamic, waitForSignal, timeoutMs, since) {
344
+ // `element` is deliberately dropped: the step runner already asserts expect.element.testid against
345
+ // the live DOM before we get here, and re-asserting it through the predicate engine would be a
346
+ // second round trip for the same fact.
347
+ const { element: _element, ...consequences } = expect;
348
+ const predicate = successToPredicate(consequences, dynamic);
349
+ if (predicate === undefined)
350
+ return undefined;
292
351
  const verdict = await waitForSignal(session, predicate, timeoutMs, since);
293
352
  if (verdict.pass)
294
353
  return undefined;
295
354
  return {
296
- reasonKind: DriftReason.STATE_MISMATCH,
297
- reason: verdict.failureReason ?? `state '${state.path}' did not hold`,
298
- anchor: `state:${state.path}`,
355
+ // The store case keeps its own kind because heal and the run report branch on it; everything
356
+ // else is a consequence that did not hold, and the reason carries observed-vs-expected.
357
+ reasonKind: expect.state !== undefined ? DriftReason.STATE_MISMATCH : DriftReason.SIGNAL_NOT_OBSERVED,
358
+ reason: verdict.failureReason ?? "the step's declared consequence did not hold",
359
+ anchor: expectLabel(expect),
299
360
  nearest: null,
300
361
  };
301
362
  }
363
+ /** Name the thing that was asserted, for the drift's `anchor` column. */
364
+ function expectLabel(expect) {
365
+ if (expect.signal !== undefined)
366
+ return `signal:${expect.signal}`;
367
+ if (expect.net !== undefined)
368
+ return `net:${expect.net.urlContains ?? expect.net.method ?? '*'}`;
369
+ if (expect.state !== undefined)
370
+ return `state:${expect.state.path}`;
371
+ if (expect.console !== undefined)
372
+ return `console:${expect.console.level ?? '*'}`;
373
+ return 'expect';
374
+ }
302
375
  /** Run one signal-anchored step: wait for the signal predicate, else drift (no nearest for signals). */
303
376
  async function runSignalStep(session, step, index, name, waitForSignal, signalTimeoutMs, since) {
304
377
  // Scope to THIS replay's floor, not the whole buffer.
@@ -310,7 +383,7 @@ async function runSignalStep(session, step, index, name, waitForSignal, signalTi
310
383
  // satisfied a LATER flow's signal step even when that flow's own action never fired it — a
311
384
  // cross-flow false green on the exact suite-verify path the regression-cost claim rests on. The
312
385
  // replay-start floor excludes prior flows/runs while still seeing this run's adjacent-step signal.
313
- const verdict = await waitForSignal(session, { kind: 'signal', name }, signalTimeoutMs, since);
386
+ const verdict = await waitForSignal(session, { kind: PredicateKind.SIGNAL, name }, signalTimeoutMs, since);
314
387
  if (verdict.pass)
315
388
  return { step: index, tool: step.tool, anchor: name, ok: true };
316
389
  return {
@@ -348,28 +421,39 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
348
421
  const page = currentRoute(session);
349
422
  // Event-time floor so the consequence reflects only THIS step's aftermath, not prior steps'.
350
423
  const cursorBefore = session.elapsed();
351
- let result;
352
- if (step.anchor.kind === AnchorKind.SIGNAL) {
353
- result = await runSignalStep(session, step, index, label, waitForSignal, signalTimeoutMs, replayFloor);
354
- }
355
- else if (step.anchor.kind === AnchorKind.COMPONENT) {
356
- result = await runComponentStep(session, step, index, step.anchor, confirmDangerous, sleep);
357
- }
358
- else if (step.anchor.kind === AnchorKind.ROLE && step.anchor.name !== undefined) {
359
- // A NAMED role anchor addresses one element. The nameless one is the degraded placeholder and
360
- // keeps its old path, where it fails legibly rather than querying a role as if it were a testid.
361
- result = await runRoleStep(session, step, index, step.anchor, confirmDangerous, sleep);
362
- }
363
- else {
364
- result = await runTestidStep(session, step, index, label, dynamic, confirmDangerous, sleep);
365
- }
366
- // A step may additionally assert STORE TRUTH (expect.state) once its action has run — caught
367
- // deterministically here, in the same cheap replay loop, with no LLM.
368
- if (result.ok && result.drift === undefined && step.expect?.state !== undefined) {
369
- const stateDrift = await assertStepState(session, step.expect.state, waitForSignal, signalTimeoutMs, cursorBefore);
370
- if (stateDrift !== undefined) {
424
+ const subSteps = step.steps;
425
+ // Traced per step, so a slow replay says WHICH step and which anchor kind spent the time. The
426
+ // per-step `durationMs` below is what the agent gets back; this is what a developer profiling
427
+ // the replay engine gets, nested under the tool call with the browser round-trips beneath it.
428
+ const result = await span('flow.step', { index, anchor: step.anchor.kind, label }, async () => {
429
+ if (isDegradedAnchor(step.anchor)) {
430
+ // Never QUERY the sentinel — it marks "no anchor was determined", not an element to find.
431
+ return degradedStepResult(step, index, label);
432
+ }
433
+ if (subSteps !== undefined && subSteps.length > 0) {
434
+ return runSequenceStep(session, step, index, subSteps, confirmDangerous, sleep);
435
+ }
436
+ if (step.anchor.kind === AnchorKind.SIGNAL) {
437
+ return runSignalStep(session, step, index, label, waitForSignal, signalTimeoutMs, replayFloor);
438
+ }
439
+ if (step.anchor.kind === AnchorKind.COMPONENT) {
440
+ return runComponentStep(session, step, index, step.anchor, confirmDangerous, sleep);
441
+ }
442
+ if (step.anchor.kind === AnchorKind.ROLE && step.anchor.name !== undefined) {
443
+ // A NAMED role anchor addresses one element. The nameless one is the degraded placeholder
444
+ // and keeps its old path, where it fails legibly rather than querying a role as a testid.
445
+ return runRoleStep(session, step, index, step.anchor, confirmDangerous, sleep);
446
+ }
447
+ return runTestidStep(session, step, index, label, dynamic, confirmDangerous, sleep);
448
+ });
449
+ // Once the anchor resolved and the action ran, the step's own expect is evaluated — signal, net,
450
+ // console and store truth alike — deterministically, in the same cheap replay loop, with no LLM.
451
+ const stepExpect = step.expect;
452
+ if (result.ok && result.drift === undefined && stepExpect !== undefined) {
453
+ const expectDrift = await assertStepExpect(session, stepExpect, dynamic, waitForSignal, signalTimeoutMs, cursorBefore);
454
+ if (expectDrift !== undefined) {
371
455
  result.ok = false;
372
- result.drift = stateDrift;
456
+ result.drift = expectDrift;
373
457
  }
374
458
  }
375
459
  if (page !== undefined)
@@ -61,7 +61,7 @@ function evidenceList(steps) {
61
61
  seen.add(part.trim());
62
62
  }
63
63
  }
64
- if (seen.size === 0)
64
+ if (0 === seen.size)
65
65
  return '_No observable consequence captured._';
66
66
  return [...seen].map((e) => `- ${e}`).join('\n');
67
67
  }
@@ -4,12 +4,7 @@
4
4
  * app's flows. These pure helpers pick the flows that belong on a given session's panel and shape them
5
5
  * into replay chips — testable without a bridge or a filesystem.
6
6
  */
7
- import { type FlowFile } from '@reticlehq/core';
8
- /** A replayable flow as the HUD renders it: a name + an optional page it can start from. */
9
- export interface FlowChip {
10
- name: string;
11
- start?: string;
12
- }
7
+ import { type FlowChip, type FlowFile } from '@reticlehq/core';
13
8
  /**
14
9
  * A flow belongs on a session's HUD when it carries no projectId (legacy/global — visible everywhere
15
10
  * for back-compat) OR its projectId matches the connecting session's. A flow stamped for project A
@@ -8,16 +8,36 @@
8
8
  */
9
9
  import { AnchorKind, type FlowAnchor, type FlowStep, type FlowStepResult } from '@reticlehq/core';
10
10
  import type { FlowReplaySession, Sleep } from './flow-replay.js';
11
- /** Run one role+name-anchored step: re-resolve via QUERY by:'role', ACT on the live ref, else drift. */
12
11
  export declare function runRoleStep(session: FlowReplaySession, step: FlowStep, index: number, anchor: Extract<FlowAnchor, {
13
12
  kind: typeof AnchorKind.ROLE;
14
13
  }>, confirmDangerous: boolean, sleep: Sleep): Promise<FlowStepResult>;
15
- /**
16
- * Dispatch one already-resolved step. Shared so every anchor kind runs the action the same way —
17
- * including the action window, whose open/close must not depend on which anchor found the element.
18
- */
19
- export declare function actOnResolvedRef(session: FlowReplaySession, step: FlowStep, index: number, label: string, ref: string, confirmDangerous: boolean): Promise<FlowStepResult>;
20
14
  /** Run one component-anchored step: re-resolve via QUERY by:'component', ACT on the live ref, else drift. */
21
15
  export declare function runComponentStep(session: FlowReplaySession, step: FlowStep, index: number, anchor: Extract<FlowAnchor, {
22
16
  kind: typeof AnchorKind.COMPONENT;
23
17
  }>, confirmDangerous: boolean, sleep: Sleep): Promise<FlowStepResult>;
18
+ /**
19
+ * DEGRADED_ANCHOR_ROLE is a MARKER ("no anchor could be determined"), never a locator.
20
+ *
21
+ * Recognising it is most of the fix: a nameless-ROLE anchor used to fall through to the testid
22
+ * runner, which asked the DOM eight times for a testid literally named "unresolved", found none (it
23
+ * never exists), and reported a MISSING ELEMENT. It then ran edit-distance against the word
24
+ * "unresolved" and offered the nearest testid as a rebind target — which flow_verify printed while
25
+ * flow_heal refused it on confidence. Two tools contradicting each other over a candidate that never
26
+ * meant anything.
27
+ *
28
+ * Both recorders emit this sentinel, so the check belongs here on the replay side, where all of them
29
+ * land.
30
+ */
31
+ export declare function isDegradedAnchor(anchor: FlowAnchor): boolean;
32
+ export declare function degradedStepResult(step: FlowStep, index: number, label: string): FlowStepResult;
33
+ /**
34
+ * Run an act_sequence step: resolve every sub-step's OWN anchor, then dispatch the whole thing as one
35
+ * ACT_SEQUENCE — the same shape `replayProgram` already uses, so a recorded sequence and a saved one
36
+ * execute identically.
37
+ *
38
+ * `replayFlow` had no sequence branch at all. It dispatches on `anchor.kind`, so a saved sequence fell
39
+ * to the testid runner and ran ONE act with `action: ''` (a saved sequence carries no top-level
40
+ * action) — sub-steps 2..n never executed. Fixing the anchor without this would have turned a visible
41
+ * drift into a silent partial replay reporting ok.
42
+ */
43
+ export declare function runSequenceStep(session: FlowReplaySession, step: FlowStep, index: number, subs: readonly FlowStep[], confirmDangerous: boolean, sleep: Sleep): Promise<FlowStepResult>;
@@ -6,10 +6,12 @@
6
6
  * point: the action window must open and close identically no matter which anchor found the element,
7
7
  * or a step's events land in the wrong window depending on how it was addressed.
8
8
  */
9
- import { AnchorKind, DriftReason, QueryBy, ReticleCommand, } from '@reticlehq/core';
9
+ import { AnchorKind, DEGRADED_ANCHOR_ROLE, DriftReason, QueryBy, ReticleCommand, } from '@reticlehq/core';
10
10
  import { ReticleTool } from '../tools/tool-names.js';
11
11
  import { replayActionArgs } from './replay.js';
12
- import { componentLabel, componentQueryArgs, resolveQuery } from './flow-replay.js';
12
+ import { anchorLabel, componentLabel, componentQueryArgs, resolveQuery, testidDrift, } from './flow-replay.js';
13
+ import { nearestRoleName } from './role-anchor-nearest.js';
14
+ import { roleDriftReason } from './role-drift-reason.js';
13
15
  /** Query args for a NAMED role anchor — the handle that identifies an instance, not a JSX site. */
14
16
  function roleQueryArgs(anchor) {
15
17
  return {
@@ -19,11 +21,53 @@ function roleQueryArgs(anchor) {
19
21
  };
20
22
  }
21
23
  /** Run one role+name-anchored step: re-resolve via QUERY by:'role', ACT on the live ref, else drift. */
24
+ /**
25
+ * The controls actually on the page, as role + name, for the nearest-match scan.
26
+ *
27
+ * Reads the interactive snapshot rather than a second query per candidate: one round trip, and it is
28
+ * the same view the coverage tool uses, so "what is on this page" has one definition.
29
+ */
30
+ async function nearestRoleNameOnPage(session, anchor) {
31
+ const wanted = 'string' === typeof anchor.name ? anchor.name : undefined;
32
+ if (wanted === undefined)
33
+ return null;
34
+ try {
35
+ // Straight off the QUERY result: resolveQuery reduces to refs, and the NAMES are what this needs.
36
+ const result = await session.command(ReticleCommand.QUERY, {
37
+ by: QueryBy.ROLE,
38
+ value: anchor.role,
39
+ });
40
+ const payload = result.result;
41
+ const elements = 'object' === typeof payload && payload !== null
42
+ ? payload.elements
43
+ : undefined;
44
+ if (!Array.isArray(elements))
45
+ return null;
46
+ const candidates = elements
47
+ .map((element) => 'object' === typeof element && element !== null
48
+ ? element.name
49
+ : undefined)
50
+ .filter((name) => 'string' === typeof name && name.length > 0)
51
+ .map((name) => ({ role: anchor.role, name }));
52
+ return nearestRoleName(anchor.role, wanted, candidates);
53
+ }
54
+ catch {
55
+ // A drift report must never fail because the suggestion lookup did.
56
+ return null;
57
+ }
58
+ }
22
59
  export async function runRoleStep(session, step, index, anchor, confirmDangerous, sleep) {
23
60
  const label = `${anchor.role} "${String(anchor.name)}"`;
24
61
  const { refs } = await resolveQuery(session, roleQueryArgs(anchor), sleep);
25
62
  const ref = refs[0];
26
63
  if (ref === undefined) {
64
+ // `nearest` used to be the literal null, so heal answered "no nearest match cleared the
65
+ // confidence floor" for EVERY role-anchored drift — a structural limit reported as a judgement
66
+ // about candidates, when nothing had looked. It looks now, and it is deliberately stricter than
67
+ // the testid path: a role name is user-visible text, so a near-match is far more likely to be a
68
+ // different control than a renamed one. A candidate here is INFORMATION for the agent; heal
69
+ // still refuses to rebind a role anchor on its own.
70
+ const nearest = await nearestRoleNameOnPage(session, anchor);
27
71
  return {
28
72
  step: index,
29
73
  tool: step.tool,
@@ -31,9 +75,9 @@ export async function runRoleStep(session, step, index, anchor, confirmDangerous
31
75
  ok: false,
32
76
  drift: {
33
77
  reasonKind: DriftReason.COMPONENT_NOT_FOUND,
34
- reason: `role anchor ${label} not found`,
78
+ reason: roleDriftReason(anchor.role, String(anchor.name), nearest),
35
79
  anchor: label,
36
- nearest: null,
80
+ nearest,
37
81
  },
38
82
  };
39
83
  }
@@ -43,7 +87,7 @@ export async function runRoleStep(session, step, index, anchor, confirmDangerous
43
87
  * Dispatch one already-resolved step. Shared so every anchor kind runs the action the same way —
44
88
  * including the action window, whose open/close must not depend on which anchor found the element.
45
89
  */
46
- export async function actOnResolvedRef(session, step, index, label, ref, confirmDangerous) {
90
+ async function actOnResolvedRef(session, step, index, label, ref, confirmDangerous) {
47
91
  session.beginAction?.(ReticleTool.FLOW_REPLAY, { ref, action: step.action ?? '' });
48
92
  let act;
49
93
  try {
@@ -65,7 +109,7 @@ export async function actOnResolvedRef(session, step, index, label, ref, confirm
65
109
  export async function runComponentStep(session, step, index, anchor, confirmDangerous, sleep) {
66
110
  const label = componentLabel(anchor);
67
111
  const { refs } = await resolveQuery(session, componentQueryArgs(anchor), sleep);
68
- if (refs.length === 0) {
112
+ if (0 === refs.length) {
69
113
  return {
70
114
  step: index,
71
115
  tool: step.tool,
@@ -100,3 +144,109 @@ export async function runComponentStep(session, step, index, anchor, confirmDang
100
144
  result.error = act.error ?? 'command failed';
101
145
  return result;
102
146
  }
147
+ /**
148
+ * DEGRADED_ANCHOR_ROLE is a MARKER ("no anchor could be determined"), never a locator.
149
+ *
150
+ * Recognising it is most of the fix: a nameless-ROLE anchor used to fall through to the testid
151
+ * runner, which asked the DOM eight times for a testid literally named "unresolved", found none (it
152
+ * never exists), and reported a MISSING ELEMENT. It then ran edit-distance against the word
153
+ * "unresolved" and offered the nearest testid as a rebind target — which flow_verify printed while
154
+ * flow_heal refused it on confidence. Two tools contradicting each other over a candidate that never
155
+ * meant anything.
156
+ *
157
+ * Both recorders emit this sentinel, so the check belongs here on the replay side, where all of them
158
+ * land.
159
+ */
160
+ export function isDegradedAnchor(anchor) {
161
+ return (anchor.kind === AnchorKind.ROLE &&
162
+ anchor.role === DEGRADED_ANCHOR_ROLE &&
163
+ anchor.name === undefined);
164
+ }
165
+ /** What a step with no resolvable anchor reports, instead of a missing-element story. */
166
+ const DEGRADED_STEP_REASON = 'recorded without a resolvable anchor (no data-testid, no accessible role+name), so it can never ' +
167
+ 'resolve on replay — add a data-testid to the element and record this flow again';
168
+ export function degradedStepResult(step, index, label) {
169
+ return {
170
+ step: index,
171
+ tool: step.tool,
172
+ anchor: label,
173
+ ok: false,
174
+ drift: {
175
+ reasonKind: DriftReason.ANCHOR_DEGRADED,
176
+ reason: DEGRADED_STEP_REASON,
177
+ anchor: label,
178
+ // Deliberately null: the old path produced a nearest match to the WORD "unresolved", which is
179
+ // not a candidate for anything.
180
+ nearest: null,
181
+ },
182
+ };
183
+ }
184
+ /** QUERY args for an element anchor — null when the anchor addresses no element. */
185
+ function anchorQueryArgs(anchor) {
186
+ if (anchor.kind === AnchorKind.TESTID)
187
+ return { by: QueryBy.TESTID, value: anchor.value };
188
+ if (anchor.kind === AnchorKind.COMPONENT)
189
+ return componentQueryArgs(anchor);
190
+ if (anchor.kind === AnchorKind.ROLE && !isDegradedAnchor(anchor))
191
+ return roleQueryArgs(anchor);
192
+ return null;
193
+ }
194
+ /**
195
+ * Run an act_sequence step: resolve every sub-step's OWN anchor, then dispatch the whole thing as one
196
+ * ACT_SEQUENCE — the same shape `replayProgram` already uses, so a recorded sequence and a saved one
197
+ * execute identically.
198
+ *
199
+ * `replayFlow` had no sequence branch at all. It dispatches on `anchor.kind`, so a saved sequence fell
200
+ * to the testid runner and ran ONE act with `action: ''` (a saved sequence carries no top-level
201
+ * action) — sub-steps 2..n never executed. Fixing the anchor without this would have turned a visible
202
+ * drift into a silent partial replay reporting ok.
203
+ */
204
+ export async function runSequenceStep(session, step, index, subs, confirmDangerous, sleep) {
205
+ const live = [];
206
+ for (const [subIndex, sub] of subs.entries()) {
207
+ const label = `${anchorLabel(sub.anchor)} (sub-step ${String(subIndex)})`;
208
+ const queryArgs = anchorQueryArgs(sub.anchor);
209
+ if (null === queryArgs)
210
+ return degradedStepResult(step, index, label);
211
+ const { refs, hint } = await resolveQuery(session, queryArgs, sleep);
212
+ const ref = refs[0];
213
+ if (ref === undefined) {
214
+ return {
215
+ step: index,
216
+ tool: step.tool,
217
+ anchor: label,
218
+ ok: false,
219
+ drift: sub.anchor.kind === AnchorKind.TESTID
220
+ ? testidDrift(sub.anchor.value, hint)
221
+ : {
222
+ reasonKind: DriftReason.COMPONENT_NOT_FOUND,
223
+ reason: `anchor ${label} not found`,
224
+ anchor: label,
225
+ nearest: null,
226
+ },
227
+ };
228
+ }
229
+ live.push({
230
+ ref,
231
+ action: sub.action ?? '',
232
+ args: replayActionArgs(sub.args, confirmDangerous),
233
+ });
234
+ }
235
+ session.beginAction?.(ReticleTool.FLOW_REPLAY, { steps: live.length });
236
+ let act;
237
+ try {
238
+ act = await session.command(ReticleCommand.ACT_SEQUENCE, { steps: live });
239
+ }
240
+ finally {
241
+ session.finishAction?.();
242
+ }
243
+ const result = {
244
+ step: index,
245
+ tool: step.tool,
246
+ anchor: anchorLabel(step.anchor),
247
+ ok: act.ok,
248
+ };
249
+ if (!act.ok)
250
+ result.error = act.error ?? 'command failed';
251
+ return result;
252
+ }
@@ -12,7 +12,7 @@
12
12
  * tool and the `@reticlehq/test` spec runner share one implementation — no divergent oracle.
13
13
  * Pure: no IO, no clock.
14
14
  */
15
- import { AnchorKind } from '@reticlehq/core';
15
+ import { AnchorKind, PredicateKind } from '@reticlehq/core';
16
16
  /** The dynamic (LLM-output) testids whose presence is never asserted — same rule replay uses. */
17
17
  export function dynamicTestids(flow) {
18
18
  return new Set((flow.dynamic ?? [])
@@ -28,7 +28,7 @@ export function successLabel(success) {
28
28
  if (success.net !== undefined)
29
29
  return success.net.urlContains ?? success.net.method ?? 'net';
30
30
  if (success.console !== undefined) {
31
- return `console:${success.console.absent === true ? 'clean' : (success.console.level ?? 'error')}`;
31
+ return `console:${true === success.console.absent ? 'clean' : (success.console.level ?? 'error')}`;
32
32
  }
33
33
  if (success.state !== undefined)
34
34
  return `state:${success.state.path}`;
@@ -39,11 +39,11 @@ export function successToPredicate(success, dynamic) {
39
39
  const parts = [];
40
40
  if (success.signal !== undefined) {
41
41
  parts.push(success.signalData !== undefined
42
- ? { kind: 'signal', name: success.signal, dataMatches: success.signalData }
43
- : { kind: 'signal', name: success.signal });
42
+ ? { kind: PredicateKind.SIGNAL, name: success.signal, dataMatches: success.signalData }
43
+ : { kind: PredicateKind.SIGNAL, name: success.signal });
44
44
  }
45
45
  if (success.net !== undefined) {
46
- const net = { kind: 'net' };
46
+ const net = { kind: PredicateKind.NET };
47
47
  if (success.net.method !== undefined)
48
48
  net.method = success.net.method;
49
49
  if (success.net.urlContains !== undefined)
@@ -56,12 +56,14 @@ export function successToPredicate(success, dynamic) {
56
56
  // exact count (e.g. 1) is transiently satisfied the instant the FIRST matching request lands —
57
57
  // before a double-submit's duplicate arrives. Gating on `settled` forces the count to be read
58
58
  // only after the network has gone quiet, so the duplicate IS counted and the over-count fails.
59
- parts.push({ kind: 'settled' });
59
+ parts.push({ kind: PredicateKind.SETTLED });
60
60
  }
61
61
  parts.push(net);
62
62
  }
63
63
  if (success.console !== undefined) {
64
- const con = { kind: 'console' };
64
+ const con = {
65
+ kind: PredicateKind.CONSOLE,
66
+ };
65
67
  if (success.console.level !== undefined)
66
68
  con.level = success.console.level;
67
69
  if (success.console.absent !== undefined) {
@@ -70,13 +72,16 @@ export function successToPredicate(success, dynamic) {
70
72
  // error yet" at the first poll and passes BEFORE the action's error fires. Gate on `settled` so
71
73
  // the console is read only after the page quiets, by which point any error is in the buffer.
72
74
  if (success.console.absent)
73
- parts.push({ kind: 'settled' });
75
+ parts.push({ kind: PredicateKind.SETTLED });
74
76
  }
75
77
  parts.push(con);
76
78
  }
77
79
  const state = success.state;
78
80
  if (state !== undefined) {
79
- const part = { kind: 'state', path: state.path };
81
+ const part = {
82
+ kind: PredicateKind.STATE,
83
+ path: state.path,
84
+ };
80
85
  if (state.store !== undefined)
81
86
  part.store = state.store;
82
87
  if (state.equals !== undefined)
@@ -85,8 +90,8 @@ export function successToPredicate(success, dynamic) {
85
90
  // wait-for-change. A wait-until-true waiter would pass the instant the path already equals the
86
91
  // value — which, for "an unrelated path stayed put", is true BEFORE a side-effect leak fires. Gate
87
92
  // on `settled` so the read happens after the page quiets, by which point the leak has landed.
88
- if (state.hold === true)
89
- parts.push({ kind: 'settled' });
93
+ if (true === state.hold)
94
+ parts.push({ kind: PredicateKind.SETTLED });
90
95
  parts.push(part);
91
96
  }
92
97
  const element = success.element;
@@ -102,15 +107,15 @@ export function successToPredicate(success, dynamic) {
102
107
  if (element.name !== undefined)
103
108
  query['name'] = element.name;
104
109
  if (Object.keys(query).length > 0)
105
- parts.push({ kind: 'element', query });
110
+ parts.push({ kind: PredicateKind.ELEMENT, query });
106
111
  }
107
112
  }
108
113
  const [first] = parts;
109
- if (parts.length === 0)
114
+ if (0 === parts.length)
110
115
  return undefined;
111
- if (parts.length === 1 && first !== undefined)
116
+ if (1 === parts.length && first !== undefined)
112
117
  return first;
113
- return { kind: 'allOf', predicates: parts };
118
+ return { kind: PredicateKind.ALL_OF, predicates: parts };
114
119
  }
115
120
  /**
116
121
  * Assert a flow's success end-condition after replay. Delegates to the injected waitForSignal (the