@reticlehq/server 2.3.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 (424) 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} +105 -15
  8. package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +6 -11
  9. package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +21 -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/cli-parse.d.ts +143 -0
  25. package/dist/{cli-parse.js → cli/cli-parse.js} +146 -65
  26. package/dist/{cli-port.d.ts → cli/cli-port.d.ts} +15 -0
  27. package/dist/{cli-port.js → cli/cli-port.js} +29 -8
  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 +182 -93
  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.d.ts +0 -6
  88. package/dist/flows/decision.js +64 -8
  89. package/dist/flows/empty-flow.d.ts +6 -0
  90. package/dist/flows/empty-flow.js +24 -0
  91. package/dist/flows/flake-store.d.ts +0 -1
  92. package/dist/flows/flake-store.js +1 -1
  93. package/dist/flows/flake.d.ts +2 -1
  94. package/dist/flows/flake.js +2 -2
  95. package/dist/flows/flow-classify.js +12 -0
  96. package/dist/flows/flow-replay-run.js +4 -4
  97. package/dist/flows/flow-replay.d.ts +5 -7
  98. package/dist/flows/flow-replay.js +114 -47
  99. package/dist/flows/flow-report.js +1 -1
  100. package/dist/flows/flow-scope.d.ts +1 -6
  101. package/dist/flows/flow-step-runners.d.ts +26 -6
  102. package/dist/flows/flow-step-runners.js +157 -6
  103. package/dist/flows/flow-success.js +20 -15
  104. package/dist/flows/flow-tools.js +67 -13
  105. package/dist/flows/flows.js +15 -11
  106. package/dist/flows/gate.d.ts +4 -3
  107. package/dist/flows/gate.js +1 -1
  108. package/dist/flows/heal-run.d.ts +0 -8
  109. package/dist/flows/heal-run.js +5 -5
  110. package/dist/flows/heal.js +3 -3
  111. package/dist/flows/predicate-to-expect.d.ts +38 -0
  112. package/dist/flows/predicate-to-expect.js +95 -0
  113. package/dist/flows/replay.d.ts +16 -1
  114. package/dist/flows/replay.js +58 -22
  115. package/dist/flows/role-anchor-nearest.d.ts +6 -0
  116. package/dist/flows/role-anchor-nearest.js +45 -0
  117. package/dist/flows/server-verify.d.ts +1 -1
  118. package/dist/flows/server-verify.js +5 -5
  119. package/dist/flows/verify-change-tools.js +32 -10
  120. package/dist/flows/verify-change-verdict.d.ts +15 -0
  121. package/dist/flows/verify-change-verdict.js +25 -0
  122. package/dist/flows/watch-batcher.d.ts +3 -2
  123. package/dist/honesty/already-true.d.ts +2 -0
  124. package/dist/honesty/already-true.js +31 -0
  125. package/dist/honesty/blind-spots.d.ts +16 -3
  126. package/dist/honesty/blind-spots.js +24 -12
  127. package/dist/honesty/honesty.d.ts +3 -2
  128. package/dist/honesty/honesty.js +4 -4
  129. package/dist/honesty/uncaptured-bodies.js +1 -1
  130. package/dist/honesty/unread-outcome.js +2 -2
  131. package/dist/honesty/verified.d.ts +22 -3
  132. package/dist/honesty/verified.js +48 -8
  133. package/dist/http-server.js +14 -7
  134. package/dist/hunt/hunt-report.d.ts +2 -1
  135. package/dist/hunt/hunt-report.js +2 -2
  136. package/dist/index.d.ts +8 -6
  137. package/dist/index.js +32 -25
  138. package/dist/init/agent-rules.d.ts +2 -1
  139. package/dist/init/agent-rules.js +45 -7
  140. package/dist/init/app-choice.d.ts +20 -0
  141. package/dist/init/app-choice.js +25 -0
  142. package/dist/init/astro-host.d.ts +21 -0
  143. package/dist/init/astro-host.js +27 -0
  144. package/dist/init/astro-patch.d.ts +16 -0
  145. package/dist/init/astro-patch.js +240 -0
  146. package/dist/init/capabilities.d.ts +17 -0
  147. package/dist/init/capabilities.js +54 -0
  148. package/dist/init/cra.d.ts +37 -0
  149. package/dist/init/cra.js +90 -0
  150. package/dist/init/cursor.js +39 -4
  151. package/dist/init/desktop-doctor.d.ts +3 -2
  152. package/dist/init/desktop-doctor.js +4 -4
  153. package/dist/init/detect.d.ts +26 -0
  154. package/dist/init/detect.js +78 -5
  155. package/dist/init/mcp-registered.d.ts +12 -0
  156. package/dist/init/mcp-registered.js +14 -0
  157. package/dist/init/mcp.d.ts +27 -2
  158. package/dist/init/mcp.js +40 -4
  159. package/dist/init/next-patch.d.ts +39 -0
  160. package/dist/init/next-patch.js +196 -0
  161. package/dist/init/node-io.js +48 -6
  162. package/dist/init/patch-kind.d.ts +20 -0
  163. package/dist/init/patch-kind.js +10 -0
  164. package/dist/init/plan-framework.d.ts +50 -0
  165. package/dist/init/plan-framework.js +341 -0
  166. package/dist/init/plan.d.ts +119 -6
  167. package/dist/init/plan.js +230 -131
  168. package/dist/init/project-id.d.ts +5 -9
  169. package/dist/init/project-id.js +10 -22
  170. package/dist/init/run.d.ts +22 -0
  171. package/dist/init/run.js +393 -28
  172. package/dist/init/slash-command.d.ts +27 -0
  173. package/dist/init/slash-command.js +61 -0
  174. package/dist/init/snippets.d.ts +41 -3
  175. package/dist/init/snippets.js +185 -10
  176. package/dist/init/vite-config.d.ts +4 -10
  177. package/dist/init/vite-config.js +15 -7
  178. package/dist/init/workspace-apps.d.ts +23 -0
  179. package/dist/init/workspace-apps.js +68 -0
  180. package/dist/input/driven-redaction.d.ts +15 -0
  181. package/dist/input/driven-redaction.js +76 -0
  182. package/dist/input/network-detail.d.ts +9 -3
  183. package/dist/input/network-detail.js +43 -23
  184. package/dist/input/network-mock-tools.js +2 -2
  185. package/dist/input/network-mock.js +4 -4
  186. package/dist/input/real-input.js +8 -3
  187. package/dist/input/scroll-find.js +2 -2
  188. package/dist/input/scroll-tools.d.ts +0 -5
  189. package/dist/input/scroll-tools.js +11 -4
  190. package/dist/input/viewport-tools.js +2 -2
  191. package/dist/journal/ambient-store.d.ts +0 -2
  192. package/dist/journal/ambient-store.js +1 -1
  193. package/dist/journal/ambient.js +1 -1
  194. package/dist/journal/attach-journal.d.ts +2 -1
  195. package/dist/journal/deviation-report.js +6 -6
  196. package/dist/journal/deviation-service.js +1 -1
  197. package/dist/journal/envelope-store.d.ts +0 -2
  198. package/dist/journal/envelope-store.js +1 -1
  199. package/dist/journal/envelope.d.ts +2 -3
  200. package/dist/journal/envelope.js +3 -3
  201. package/dist/journal/journal-query.js +1 -1
  202. package/dist/journal/journal-recorder.d.ts +2 -1
  203. package/dist/journal/journal-recorder.js +2 -2
  204. package/dist/journal/rollups.js +9 -9
  205. package/dist/journal/session-end.d.ts +2 -1
  206. package/dist/journal/session-journal.js +4 -4
  207. package/dist/license/license.d.ts +0 -14
  208. package/dist/license/license.js +8 -8
  209. package/dist/log.d.ts +10 -0
  210. package/dist/log.js +11 -1
  211. package/dist/mcp/mcp-is-error.d.ts +13 -0
  212. package/dist/mcp/mcp-is-error.js +18 -0
  213. package/dist/mcp/mcp-outage.d.ts +31 -0
  214. package/dist/mcp/mcp-outage.js +48 -0
  215. package/dist/mcp/mcp-proxy.d.ts +124 -0
  216. package/dist/mcp/mcp-proxy.js +697 -0
  217. package/dist/{mcp.d.ts → mcp/mcp.d.ts} +13 -10
  218. package/dist/{mcp.js → mcp/mcp.js} +110 -35
  219. package/dist/mcp/proxy-handshake.d.ts +3 -0
  220. package/dist/mcp/proxy-handshake.js +54 -0
  221. package/dist/mcp/proxy-lifecycle.d.ts +64 -0
  222. package/dist/mcp/proxy-lifecycle.js +70 -0
  223. package/dist/mcp/tool-catalog-cache.d.ts +29 -0
  224. package/dist/mcp/tool-catalog-cache.js +55 -0
  225. package/dist/oracles/propose-consequences.d.ts +2 -1
  226. package/dist/oracles/propose-consequences.js +16 -11
  227. package/dist/oracles/self-instrument.d.ts +2 -1
  228. package/dist/oracles/self-instrument.js +1 -1
  229. package/dist/phenomena/phenomena.js +3 -3
  230. package/dist/platform.d.ts +16 -0
  231. package/dist/platform.js +16 -0
  232. package/dist/pool/playwright-launcher.d.ts +15 -2
  233. package/dist/pool/playwright-launcher.js +21 -2
  234. package/dist/project/fs-port.js +1 -1
  235. package/dist/project/project-tools.js +2 -2
  236. package/dist/runs/build-verification-run.js +1 -1
  237. package/dist/runs/repair-prompt.d.ts +0 -6
  238. package/dist/runs/repair-prompt.js +3 -3
  239. package/dist/runs/risk-classify.js +1 -1
  240. package/dist/runs/run-diff.d.ts +0 -2
  241. package/dist/runs/run-diff.js +3 -3
  242. package/dist/runs/run-tools.js +2 -2
  243. package/dist/runs/verification-sync.js +2 -2
  244. package/dist/runs/verify-http.js +1 -1
  245. package/dist/runs/verify-server.js +1 -1
  246. package/dist/session/command-timeout.d.ts +11 -3
  247. package/dist/session/command-timeout.js +2 -2
  248. package/dist/session/control-envelope.d.ts +6 -0
  249. package/dist/session/control-envelope.js +7 -1
  250. package/dist/session/dev-server-probe.d.ts +13 -0
  251. package/dist/session/dev-server-probe.js +52 -0
  252. package/dist/session/last-act.d.ts +17 -3
  253. package/dist/session/last-act.js +16 -4
  254. package/dist/session/live-control-tools.js +7 -2
  255. package/dist/session/live-control.js +1 -1
  256. package/dist/session/looks-like-dev-server.d.ts +14 -0
  257. package/dist/session/looks-like-dev-server.js +25 -0
  258. package/dist/session/no-session-diagnosis.d.ts +38 -0
  259. package/dist/session/no-session-diagnosis.js +70 -0
  260. package/dist/session/no-session-watch.d.ts +19 -0
  261. package/dist/session/no-session-watch.js +61 -0
  262. package/dist/session/observed-state.d.ts +4 -0
  263. package/dist/session/observed-state.js +19 -1
  264. package/dist/session/output-budget.js +1 -1
  265. package/dist/session/session-health.d.ts +8 -1
  266. package/dist/session/session-health.js +8 -7
  267. package/dist/session/session-info.d.ts +26 -7
  268. package/dist/session/session-info.js +42 -1
  269. package/dist/session/session-lease.d.ts +0 -11
  270. package/dist/session/session-lease.js +1 -1
  271. package/dist/session/session-manager.d.ts +3 -0
  272. package/dist/session/session-manager.js +58 -9
  273. package/dist/session/session-readiness.d.ts +1 -1
  274. package/dist/session/session-readiness.js +6 -1
  275. package/dist/session/session-reaper.js +1 -1
  276. package/dist/session/session-reconnect.d.ts +32 -0
  277. package/dist/session/session-reconnect.js +42 -0
  278. package/dist/session/session.d.ts +20 -2
  279. package/dist/session/session.js +76 -26
  280. package/dist/session/state-select.d.ts +1 -1
  281. package/dist/session/state-select.js +1 -1
  282. package/dist/status-payload.d.ts +19 -0
  283. package/dist/status-payload.js +11 -0
  284. package/dist/telemetry/argument-shape.js +10 -1
  285. package/dist/telemetry/browser-mode.d.ts +27 -0
  286. package/dist/telemetry/browser-mode.js +33 -0
  287. package/dist/telemetry/bug-found.d.ts +17 -21
  288. package/dist/telemetry/bug-found.js +120 -8
  289. package/dist/telemetry/cli-telemetry.d.ts +2 -12
  290. package/dist/telemetry/cli-telemetry.js +27 -4
  291. package/dist/telemetry/daemon-telemetry.d.ts +19 -0
  292. package/dist/telemetry/daemon-telemetry.js +12 -2
  293. package/dist/telemetry/dev-repo.js +4 -4
  294. package/dist/telemetry/error-fingerprint.js +21 -2
  295. package/dist/telemetry/feedback-cli.d.ts +1 -1
  296. package/dist/telemetry/feedback-cli.js +4 -4
  297. package/dist/telemetry/feedback-context.d.ts +22 -0
  298. package/dist/telemetry/feedback-context.js +41 -1
  299. package/dist/telemetry/feedback-delivery.d.ts +20 -0
  300. package/dist/telemetry/feedback-delivery.js +30 -0
  301. package/dist/telemetry/feedback-outbox.d.ts +22 -0
  302. package/dist/telemetry/feedback-outbox.js +87 -0
  303. package/dist/telemetry/feedback.d.ts +22 -1
  304. package/dist/telemetry/feedback.js +67 -8
  305. package/dist/telemetry/git-facts.js +1 -1
  306. package/dist/telemetry/session-metrics.d.ts +2 -0
  307. package/dist/telemetry/session-metrics.js +68 -6
  308. package/dist/telemetry/telemetry.d.ts +27 -2
  309. package/dist/telemetry/telemetry.js +119 -29
  310. package/dist/telemetry/verification-of.d.ts +22 -0
  311. package/dist/telemetry/verification-of.js +84 -0
  312. package/dist/tools/act-capsule.js +2 -2
  313. package/dist/tools/act-danger.d.ts +12 -0
  314. package/dist/tools/act-danger.js +20 -3
  315. package/dist/tools/act-helpers.js +4 -4
  316. package/dist/tools/act-tools.d.ts +0 -8
  317. package/dist/tools/act-tools.js +84 -115
  318. package/dist/tools/act-view.js +7 -7
  319. package/dist/tools/alias-args.d.ts +17 -0
  320. package/dist/tools/alias-args.js +25 -0
  321. package/dist/tools/assert-grade.js +9 -9
  322. package/dist/tools/assert-verdict.d.ts +3 -1
  323. package/dist/tools/assert-verdict.js +8 -5
  324. package/dist/tools/browser-tools.js +30 -11
  325. package/dist/tools/contract-tools.js +1 -1
  326. package/dist/tools/coverage-identity.d.ts +34 -0
  327. package/dist/tools/coverage-identity.js +46 -0
  328. package/dist/tools/coverage-tools.js +10 -14
  329. package/dist/tools/dynamic-tools.d.ts +2 -1
  330. package/dist/tools/dynamic-tools.js +90 -18
  331. package/dist/tools/error-recovery.d.ts +20 -2
  332. package/dist/tools/error-recovery.js +179 -4
  333. package/dist/tools/feedback-tools.d.ts +12 -1
  334. package/dist/tools/feedback-tools.js +28 -5
  335. package/dist/tools/invoke-tool.d.ts +0 -6
  336. package/dist/tools/invoke-tool.js +57 -32
  337. package/dist/tools/lease-hint.d.ts +17 -0
  338. package/dist/tools/lease-hint.js +24 -0
  339. package/dist/tools/lease-tools.js +6 -5
  340. package/dist/tools/merge-tools.d.ts +10 -1
  341. package/dist/tools/merge-tools.js +9 -3
  342. package/dist/tools/merged-name-redirect.d.ts +13 -0
  343. package/dist/tools/merged-name-redirect.js +55 -0
  344. package/dist/tools/navigate-result.d.ts +17 -0
  345. package/dist/tools/navigate-result.js +31 -0
  346. package/dist/tools/observe-tools.js +54 -20
  347. package/dist/tools/observed-nothing.d.ts +25 -0
  348. package/dist/tools/observed-nothing.js +33 -0
  349. package/dist/tools/query-paginate.js +2 -2
  350. package/dist/tools/query-shape.d.ts +18 -0
  351. package/dist/tools/query-shape.js +39 -0
  352. package/dist/tools/read-tools.js +85 -20
  353. package/dist/tools/real-input-attempt.d.ts +28 -0
  354. package/dist/tools/real-input-attempt.js +88 -0
  355. package/dist/tools/reconcile-tools.js +7 -7
  356. package/dist/tools/reload-result.d.ts +14 -0
  357. package/dist/tools/reload-result.js +29 -0
  358. package/dist/tools/snapshot-delta.d.ts +4 -5
  359. package/dist/tools/snapshot-delta.js +0 -0
  360. package/dist/tools/tool-kit.d.ts +8 -0
  361. package/dist/tools/tool-surface.d.ts +83 -0
  362. package/dist/tools/tool-surface.js +204 -0
  363. package/dist/tools/tools-helpers.js +6 -6
  364. package/dist/tools/tools.d.ts +15 -0
  365. package/dist/tools/tools.js +53 -17
  366. package/dist/tools/unadvertised-help.d.ts +5 -0
  367. package/dist/tools/unadvertised-help.js +39 -0
  368. package/dist/trace.d.ts +69 -0
  369. package/dist/trace.js +150 -0
  370. package/dist/update/nudge-credit.d.ts +4 -0
  371. package/dist/update/nudge-credit.js +53 -0
  372. package/dist/update/sdk-sync.d.ts +21 -0
  373. package/dist/update/sdk-sync.js +42 -0
  374. package/dist/update/update-checker.js +3 -3
  375. package/dist/update/update-nudge.d.ts +63 -6
  376. package/dist/update/update-nudge.js +128 -15
  377. package/dist/update/updater.d.ts +1 -9
  378. package/dist/update/updater.js +13 -8
  379. package/dist/version/peer-announce.d.ts +4 -0
  380. package/dist/version/peer-announce.js +30 -0
  381. package/dist/{server-version.js → version/server-version.js} +5 -1
  382. package/dist/version/version-nudge.d.ts +28 -0
  383. package/dist/version/version-nudge.js +40 -0
  384. package/dist/version/version-skew.d.ts +66 -0
  385. package/dist/version/version-skew.js +71 -0
  386. package/dist/visual/visual-diff.js +2 -2
  387. package/dist/visual/visual-tools.js +4 -4
  388. package/docs/agent-cheatsheet.md +85 -0
  389. package/docs/architecture.md +113 -0
  390. package/docs/benchmarks.md +255 -0
  391. package/docs/debugging.md +102 -0
  392. package/docs/deploy-checks.md +69 -0
  393. package/docs/desktop-apps.md +256 -0
  394. package/docs/enterprise.md +62 -0
  395. package/docs/fixtures.md +28 -0
  396. package/docs/flows.md +166 -0
  397. package/docs/getting-started.md +469 -0
  398. package/docs/human-control.md +63 -0
  399. package/docs/integration-patterns.md +171 -0
  400. package/docs/local-registry.md +126 -0
  401. package/docs/mint.json +61 -0
  402. package/docs/multi-agent-testing.md +55 -0
  403. package/docs/platform-integration.md +122 -0
  404. package/docs/telemetry-contract.md +214 -0
  405. package/docs/telemetry.md +184 -0
  406. package/docs/testing.md +82 -0
  407. package/docs/token-efficiency.md +80 -0
  408. package/docs/usage.md +1046 -0
  409. package/package.json +8 -5
  410. package/dist/cli-launch.js +0 -112
  411. package/dist/cli-parse.d.ts +0 -140
  412. package/dist/daemon-resilience.d.ts +0 -22
  413. package/dist/daemon-resilience.js +0 -85
  414. package/dist/mcp-proxy.d.ts +0 -35
  415. package/dist/mcp-proxy.js +0 -190
  416. package/dist/tools/profiles.d.ts +0 -37
  417. package/dist/tools/profiles.js +0 -139
  418. /package/dist/{pairing-token.d.ts → bridge/pairing-token.d.ts} +0 -0
  419. /package/dist/{token-auth.d.ts → bridge/token-auth.d.ts} +0 -0
  420. /package/dist/{token-auth.js → bridge/token-auth.js} +0 -0
  421. /package/dist/{cli-doctor.d.ts → cli/cli-doctor.d.ts} +0 -0
  422. /package/dist/{cli-verify.d.ts → cli/cli-verify.d.ts} +0 -0
  423. /package/dist/{cloud-cli.d.ts → cli/cloud-cli.d.ts} +0 -0
  424. /package/dist/{server-version.d.ts → version/server-version.d.ts} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { AnchorKind, ReplayStatus, } from '@reticlehq/core';
2
- import { classifyFlowAssertions } from './flow-classify.js';
2
+ import { classifyFlowAssertions, FlowAssertionGrade } from './flow-classify.js';
3
3
  import { SUCCESS_STEP_TOOL } from './flow-success.js';
4
4
  /**
5
5
  * Turn a replay result into the autonomy DECISION envelope — the judgement a human reviewer used to
@@ -35,7 +35,7 @@ export function buildDecision(result, flow) {
35
35
  // Green — but is it green-for-the-right-reason? A flow that asserts no consequence can pass while
36
36
  // broken, so the honest next action is to add one.
37
37
  const grade = flow !== undefined ? classifyFlowAssertions(flow) : undefined;
38
- const verifiesOutcome = grade?.hasConsequenceAssertion === true;
38
+ const verifiesOutcome = true === grade?.hasConsequenceAssertion;
39
39
  const intent = flow?.intent;
40
40
  return {
41
41
  verdict: 'pass',
@@ -92,16 +92,51 @@ function suiteVerdictOf(status) {
92
92
  * decision-annotated replay results + their flows. Passing flows are counted; only failures carry
93
93
  * detail (token-cheap), each with the actionable decision so the agent fixes without re-querying.
94
94
  */
95
+ /**
96
+ * Why a green replay still verified nothing, or undefined when it genuinely did.
97
+ *
98
+ * A flow with no steps, or one that asserts no observable consequence, replays green no matter what
99
+ * the app does — the grader already knows this at save time and says so. Counting it as `passed`
100
+ * turned that warning into "all 1 flow pass", which is a false green in the exact feature sold as
101
+ * the regression suite. Classified only when the flow file is available; never guessed.
102
+ */
103
+ function unverifiableReason(flow) {
104
+ if (flow === undefined)
105
+ return undefined;
106
+ if (0 === flow.steps.length) {
107
+ return 'the flow has no steps — it replays green whatever the app does. Record it again, or add steps with reticle_annotate.';
108
+ }
109
+ const c = classifyFlowAssertions(flow);
110
+ if (c.grade !== FlowAssertionGrade.ASSERTION_FREE)
111
+ return undefined;
112
+ return (c.warning ??
113
+ 'the flow asserts no observable consequence, so it passes even when the feature is broken.');
114
+ }
95
115
  export function buildSuiteVerdict(runs) {
96
116
  const failures = [];
117
+ const unverifiable = [];
97
118
  let passed = 0;
98
119
  for (const { replay, flow } of runs) {
99
120
  if (replay.status === ReplayStatus.OK) {
100
- passed += 1;
121
+ const reason = unverifiableReason(flow);
122
+ // A green that cannot go red is not a pass. Counted apart, so `passed` stays a count of things
123
+ // actually verified rather than of replays that merely completed.
124
+ if (reason !== undefined)
125
+ unverifiable.push({ flow: replay.name, reason });
126
+ else
127
+ passed += 1;
101
128
  continue;
102
129
  }
103
130
  const decision = replay.decision ?? buildDecision(replay, flow);
104
131
  const row = { flow: replay.name, verdict: suiteVerdictOf(replay.status) };
132
+ // Zero step results PLUS an error is the signature of a replay that never STARTED — the flow file
133
+ // failed to load (before the session is even resolved), or its leased context never came up. A
134
+ // flow that ran and failed always carries its failing step; a flow with no steps replays OK and
135
+ // never reaches here. Both arrive as ReplayStatus.ERROR, so without this the suite cannot tell
136
+ // "the app broke" from "Reticle could not run this" — and the bug metric reported the second as
137
+ // the first, 8 times in one sweep.
138
+ if (0 === replay.steps.length && replay.error !== undefined)
139
+ row.couldNotRun = true;
105
140
  if (decision.whatChanged !== undefined)
106
141
  row.whatChanged = decision.whatChanged;
107
142
  if (decision.whereInSource !== undefined)
@@ -111,9 +146,30 @@ export function buildSuiteVerdict(runs) {
111
146
  }
112
147
  const total = runs.length;
113
148
  const failed = failures.length;
114
- const status = failed === 0 ? 'pass' : 'fail';
115
- const summary = failed === 0
116
- ? `all ${total} flow${total === 1 ? '' : 's'} pass`
117
- : `${passed}/${total} flows pass — ${failed} need attention: ${failures.map((f) => f.flow).join(', ')}`;
118
- return { status, total, passed, failed, summary, failures };
149
+ // A real failure outranks an unverifiable flow: a broken flow is worse news than an empty one.
150
+ // An EMPTY suite is unverifiable for the same reason a flow that asserts nothing is — it is the
151
+ // purest green that cannot go red. It used to report `pass` with the summary "all 0 flows pass",
152
+ // which meant `reticle verify` went green on every project that had not written a flow yet, and on
153
+ // any project where the flows directory failed to resolve. Found by the adversarial MCP sweep; it
154
+ // was the only invented answer in 994 calls.
155
+ const status = failed > 0 ? 'fail' : unverifiable.length > 0 || 0 === total ? 'unverifiable' : 'pass';
156
+ const cannotFail = 0 === unverifiable.length
157
+ ? ''
158
+ : ` — ${String(unverifiable.length)} verified nothing (${unverifiable.map((u) => u.flow).join(', ')})`;
159
+ const summary = 0 === total
160
+ ? 'no flows to verify — nothing was checked. Record one with reticle_record { action: "start" }, then reticle_flow_save.'
161
+ : 0 === failed
162
+ ? 0 === unverifiable.length
163
+ ? `all ${total} flow${1 === total ? '' : 's'} pass`
164
+ : `${String(passed)}/${String(total)} flows verified${cannotFail}`
165
+ : `${passed}/${total} flows pass — ${failed} need attention: ${failures.map((f) => f.flow).join(', ')}${cannotFail}`;
166
+ return {
167
+ status,
168
+ total,
169
+ passed,
170
+ failed,
171
+ summary,
172
+ failures,
173
+ ...(unverifiable.length > 0 ? { unverifiable } : {}),
174
+ };
119
175
  }
@@ -0,0 +1,6 @@
1
+ interface EmptyFlowRefusal {
2
+ error: string;
3
+ recovery: string;
4
+ }
5
+ export declare function emptyFlowRefusal(stepCount: number, name: string): EmptyFlowRefusal | undefined;
6
+ export {};
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Refuse to save a flow with no steps.
3
+ *
4
+ * It used to succeed: `flow_save` returned `{ stepCount: 0, grade: "assertion-free" }` with no error,
5
+ * `flow_list` then showed one more flow, and `flow_verify` called it `unverifiable` — forever. The
6
+ * agent believes it saved a regression test; what it wrote is a permanent suite entry that can never
7
+ * go green or red.
8
+ *
9
+ * Zero steps is not a degraded flow, it is the absence of one. And the refusal matters most on the
10
+ * stacks where the recorder is known to capture nothing, because there the empty save is the only
11
+ * signal the agent would ever get that something went wrong.
12
+ */
13
+ import { ReticleTool } from '../tools/tool-names.js';
14
+ export function emptyFlowRefusal(stepCount, name) {
15
+ if (stepCount > 0)
16
+ return undefined;
17
+ return {
18
+ error: `'${name}' has no steps, so there is nothing to save — a flow with no steps can never pass or fail, and would sit in the suite reporting "unverifiable" forever`,
19
+ recovery: `Nothing was captured between ${ReticleTool.RECORD} { action: "start" } and { action: "stop" }. ` +
20
+ `Drive at least one action with ${ReticleTool.ACT} or ${ReticleTool.ACT_AND_WAIT} while the ` +
21
+ `recording is open, then save. If you DID act and this still says zero, the recorder captured ` +
22
+ `nothing for this stack — report it with ${ReticleTool.FEEDBACK}.`,
23
+ };
24
+ }
@@ -1,6 +1,5 @@
1
1
  import type { FileSystemPort } from '../project/fs-port.js';
2
2
  import { type FlakeLedger } from './flake.js';
3
- export declare const FLAKE_FILE_VERSION = 1;
4
3
  /**
5
4
  * Persists the per-flow flake ledger at `.reticle/flake.json`. Local single-project memory (stays OSS —
6
5
  * OSS-VS-SERVER). Loads never throw: missing/malformed/wrong-version degrades to an empty ledger, so
@@ -1,7 +1,7 @@
1
1
  import { dirname } from 'node:path';
2
2
  import { reticleDirPaths } from '../project/reticle-dir.js';
3
3
  import { FlakeFileSchema, emptyRecord, isFlaky, recordOutcome } from './flake.js';
4
- export const FLAKE_FILE_VERSION = 1;
4
+ const FLAKE_FILE_VERSION = 1;
5
5
  /**
6
6
  * Persists the per-flow flake ledger at `.reticle/flake.json`. Local single-project memory (stays OSS —
7
7
  * OSS-VS-SERVER). Loads never throw: missing/malformed/wrong-version degrades to an empty ledger, so
@@ -5,7 +5,7 @@ import { z } from 'zod';
5
5
  * (excluded from gate-blocking) and surfaced for repair, never silently blocking. The caller accrues an
6
6
  * outcome ONLY for unchanged-code replays (a fail after a code change is a legitimate red, not flake).
7
7
  */
8
- export declare const FlakeRecordSchema: z.ZodObject<{
8
+ declare const FlakeRecordSchema: z.ZodObject<{
9
9
  runs: z.ZodNumber;
10
10
  fails: z.ZodNumber;
11
11
  }, "strip", z.ZodTypeAny, {
@@ -55,3 +55,4 @@ export declare function recordOutcome(record: FlakeRecord, passed: boolean): Fla
55
55
  export declare function isFlaky(record: FlakeRecord, minRuns?: number): boolean;
56
56
  /** Observed intermittent-failure rate (0 when too few runs to judge). */
57
57
  export declare function flakeRate(record: FlakeRecord): number;
58
+ export {};
@@ -5,7 +5,7 @@ import { z } from 'zod';
5
5
  * (excluded from gate-blocking) and surfaced for repair, never silently blocking. The caller accrues an
6
6
  * outcome ONLY for unchanged-code replays (a fail after a code change is a legitimate red, not flake).
7
7
  */
8
- export const FlakeRecordSchema = z.object({
8
+ const FlakeRecordSchema = z.object({
9
9
  runs: z.number().int().min(0),
10
10
  fails: z.number().int().min(0),
11
11
  });
@@ -31,5 +31,5 @@ export function isFlaky(record, minRuns = DEFAULT_MIN_FLAKE_RUNS) {
31
31
  }
32
32
  /** Observed intermittent-failure rate (0 when too few runs to judge). */
33
33
  export function flakeRate(record) {
34
- return record.runs === 0 ? 0 : record.fails / record.runs;
34
+ return 0 === record.runs ? 0 : record.fails / record.runs;
35
35
  }
@@ -33,6 +33,18 @@ const INTENT_WITHOUT_OUTCOME_WARNING = 'This flow declares a business intent but
33
33
  // Consequence vs presence-only classification lives in @reticlehq/core (one source for all graders).
34
34
  const expectIsConsequence = flowExpectHasConsequence;
35
35
  const expectIsWeak = flowExpectIsPresenceOnly;
36
+ /**
37
+ * A step expect counts the same as a flow-level one, because replay now evaluates the same set.
38
+ *
39
+ * This was briefly narrowed to `state` only — correctly, at the time: replay checked element presence
40
+ * and state and nothing else, so grading a step signal/net as a consequence produced a flow that
41
+ * reported `grade: "asserted"` and could not go red. `assertStepExpect` closed that by compiling
42
+ * every step expect through the same `successToPredicate` the flow-level path uses, so the narrow
43
+ * rule is no longer true and would now UNDER-grade real assertions.
44
+ *
45
+ * The invariant to keep: this function and what replay enforces must move together. If one ever
46
+ * describes more than the other, the difference is a false green or a lost verification.
47
+ */
36
48
  /** Walk steps + act_sequence sub-steps so an expect on either level is counted. */
37
49
  function flattenSteps(steps) {
38
50
  const out = [];
@@ -64,7 +64,7 @@ async function recordReplayRun(deps, name, status, driftSteps, durationMs, proje
64
64
  });
65
65
  // Per-project cloud: push memory outcomes only when cloud is attached AND memory sync is enabled.
66
66
  const cloud = await resolveProjectCloud(deps.fs, deps.reticleRoot, homedir(), process.env);
67
- if (cloud.config === null || !cloud.policy.memory)
67
+ if (null === cloud.config || !cloud.policy.memory)
68
68
  return; // not attached / memory disabled → local only
69
69
  const result = await syncRunRecordToCloud({ kind: RunKind.FLOW_REPLAY, name, status: runStatus, at: deps.now(), durationMs }, projectId, cloud.config, (url, init) => fetch(url, init));
70
70
  if (result.outcome !== SyncOutcome.SYNCED) {
@@ -82,7 +82,7 @@ async function recordReplayRun(deps, name, status, driftSteps, durationMs, proje
82
82
  */
83
83
  export function startPathMismatchHint(flow, session) {
84
84
  const startPath = flow.startPath;
85
- if (startPath === undefined || startPath.length === 0)
85
+ if (startPath === undefined || 0 === startPath.length)
86
86
  return undefined;
87
87
  const routes = session.eventsSince(0).filter((e) => e.type === EventType.ROUTE_CHANGE);
88
88
  const last = routes.at(-1);
@@ -127,7 +127,7 @@ export async function replayNamedFlow(deps, args) {
127
127
  // Floor the success oracle at the start of THIS replay so a stale signal from a prior run
128
128
  // in the same session can't fake a pass.
129
129
  const replayFloor = session.elapsed();
130
- const steps = await replayFlow(session, loaded.value, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, args['confirmDangerous'] === true);
130
+ const steps = await replayFlow(session, loaded.value, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, true === args['confirmDangerous']);
131
131
  // "green means intent satisfied": when every step ran clean, assert the flow's success
132
132
  // end-condition as a real consequence. A signal/net success that never fires FAILS the replay
133
133
  // even though all locators resolved — the regression a healed-but-wrong locator ships green.
@@ -188,7 +188,7 @@ export async function replayNamedFlow(deps, args) {
188
188
  async function computeReplayDeviation(deps, session, floor) {
189
189
  try {
190
190
  const segments = computeSegments(session.eventsSince(floor));
191
- if (segments.length === 0)
191
+ if (0 === segments.length)
192
192
  return undefined;
193
193
  return await reportAndAccumulate(new EnvelopeStore(deps.fs, deps.reticleRoot), segments);
194
194
  }
@@ -1,4 +1,4 @@
1
- import { AnchorKind, type CommandResult, type FlowAnchor, type FlowFile, type FlowStepResult, type ReticleEvent, type QueryEmptyHint } from '@reticlehq/core';
1
+ import { AnchorKind, type CommandResult, type Drift, type FlowAnchor, type FlowFile, type FlowStepResult, type ReticleEvent, type QueryEmptyHint } from '@reticlehq/core';
2
2
  import type { EvalResult, Predicate } from '../events/predicate.js';
3
3
  /**
4
4
  * The session surface flow-replay needs: QUERY to re-resolve a testid anchor against the live
@@ -47,12 +47,8 @@ export type Sleep = (ms: number) => Promise<void>;
47
47
  * Such a drift is surfaced (with a nearest) but never auto-healed.
48
48
  */
49
49
  export declare function nearestIsAmbiguous(missing: string, present: string[]): boolean;
50
- /**
51
- * Re-resolve any QUERY against the live DOM, tolerating an in-flight render: QUERY, and while it
52
- * returns zero refs, sleep and retry up to ANCHOR_SETTLE_ATTEMPTS. Returns as soon as refs appear,
53
- * so a present anchor costs one query; a genuinely missing one costs the full (bounded) settle and
54
- * then drifts. The last result's near-miss hint is returned for the drift record.
55
- */
50
+ /** Build the legible-drift record for a testid anchor that resolved to zero live elements. */
51
+ export declare function testidDrift(value: string, hint: QueryEmptyHint | undefined): Drift;
56
52
  export declare function resolveQuery(session: FlowReplaySession, queryArgs: Record<string, unknown>, sleep: Sleep): Promise<{
57
53
  refs: string[];
58
54
  hint?: QueryEmptyHint;
@@ -61,6 +57,8 @@ export declare function resolveQuery(session: FlowReplaySession, queryArgs: Reco
61
57
  export declare function componentLabel(anchor: Extract<FlowAnchor, {
62
58
  kind: typeof AnchorKind.COMPONENT;
63
59
  }>): string;
60
+ /** The value of a step's primary anchor, for labelling the result row. */
61
+ export declare function anchorLabel(anchor: FlowAnchor): string;
64
62
  /** QUERY args for a component auto-anchor — source (precise) + component name (coarse) as given. */
65
63
  export declare function componentQueryArgs(anchor: Extract<FlowAnchor, {
66
64
  kind: typeof AnchorKind.COMPONENT;
@@ -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.
@@ -67,10 +69,10 @@ function readQuery(result) {
67
69
  return { refs };
68
70
  const payload = asRecord(result.result);
69
71
  const rawHint = payload['hint'];
70
- if (typeof rawHint === 'object' && rawHint !== null) {
72
+ if ('object' === typeof rawHint && rawHint !== null) {
71
73
  const hint = asRecord(rawHint);
72
74
  const present = Array.isArray(hint['presentTestids'])
73
- ? hint['presentTestids'].filter((t) => typeof t === 'string')
75
+ ? hint['presentTestids'].filter((t) => 'string' === typeof t)
74
76
  : [];
75
77
  return {
76
78
  refs,
@@ -78,7 +80,7 @@ function readQuery(result) {
78
80
  route: asString(hint['route']) ?? '',
79
81
  presentTestids: present,
80
82
  presentRegions: [],
81
- knownEmptyState: hint['knownEmptyState'] === true,
83
+ knownEmptyState: true === hint['knownEmptyState'],
82
84
  },
83
85
  };
84
86
  }
@@ -107,7 +109,7 @@ export function nearestIsAmbiguous(missing, present) {
107
109
  return count >= 2;
108
110
  }
109
111
  /** Build the legible-drift record for a testid anchor that resolved to zero live elements. */
110
- function testidDrift(value, hint) {
112
+ export function testidDrift(value, hint) {
111
113
  const present = hint?.presentTestids ?? [];
112
114
  const drift = {
113
115
  reasonKind: DriftReason.TESTID_NOT_FOUND,
@@ -125,10 +127,41 @@ function testidDrift(value, hint) {
125
127
  * so a present anchor costs one query; a genuinely missing one costs the full (bounded) settle and
126
128
  * then drifts. The last result's near-miss hint is returned for the drift record.
127
129
  */
130
+ /**
131
+ * One wait between anchor attempts: the fixed tick, or the next DOM event — whichever comes first.
132
+ *
133
+ * An element mounting IS a mutation, and the session already streams those, so sleeping out the rest
134
+ * of a 150ms tick after the thing has already appeared is pure latency. Measured on next-app-router:
135
+ * a single `flow.step` span was 1079ms around nine QUERY round-trips of 1–2ms each — the cost was
136
+ * entirely the sleeping, and four such steps made up 4.3s of that app's 7.6s.
137
+ *
138
+ * It can only resolve the wait EARLIER, never end the loop earlier: the attempt budget above is
139
+ * untouched, so a genuinely missing anchor still spends the full settle before it drifts. An early
140
+ * "not found" would be a false drift, which is the failure this loop exists to prevent.
141
+ */
142
+ async function settleTick(session, sleep) {
143
+ let unsubscribe;
144
+ try {
145
+ await new Promise((resolve) => {
146
+ let done = false;
147
+ const finish = () => {
148
+ if (done)
149
+ return;
150
+ done = true;
151
+ resolve();
152
+ };
153
+ unsubscribe = session.onEvent(finish);
154
+ void sleep(ANCHOR_SETTLE_DELAY_MS).then(finish);
155
+ });
156
+ }
157
+ finally {
158
+ unsubscribe?.();
159
+ }
160
+ }
128
161
  export async function resolveQuery(session, queryArgs, sleep) {
129
162
  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);
163
+ for (let attempt = 1; 0 === last.refs.length && attempt < ANCHOR_SETTLE_ATTEMPTS; attempt += 1) {
164
+ await settleTick(session, sleep);
132
165
  last = readQuery(await session.command(ReticleCommand.QUERY, queryArgs));
133
166
  }
134
167
  return last;
@@ -188,7 +221,7 @@ function summarizeConsequence(events) {
188
221
  const data = n.data ?? {};
189
222
  const method = asString(data['method']) ?? 'GET';
190
223
  const path = trimUrl(asString(data['url']) ?? '');
191
- const status = typeof data['status'] === 'number' ? ` ${data['status']}` : '';
224
+ const status = 'number' === typeof data['status'] ? ` ${data['status']}` : '';
192
225
  parts.push(`${method} ${path}${status}`.trim());
193
226
  }
194
227
  const errors = events.filter((e) => e.type === EventType.CONSOLE_ERROR || e.type === EventType.ERROR_UNCAUGHT).length;
@@ -206,7 +239,7 @@ export function componentLabel(anchor) {
206
239
  return anchor.component ?? anchor.name ?? anchor.role ?? 'component';
207
240
  }
208
241
  /** The value of a step's primary anchor, for labelling the result row. */
209
- function anchorLabel(anchor) {
242
+ export function anchorLabel(anchor) {
210
243
  if (anchor.kind === AnchorKind.TESTID)
211
244
  return anchor.value;
212
245
  if (anchor.kind === AnchorKind.SIGNAL)
@@ -227,7 +260,7 @@ export function componentQueryArgs(anchor) {
227
260
  /** Run one testid-anchored step: re-resolve via QUERY, then ACT on the live ref, else drift. */
228
261
  async function runTestidStep(session, step, index, value, dynamic, confirmDangerous, sleep) {
229
262
  const { refs, hint } = await resolveTestid(session, value, sleep);
230
- if (refs.length === 0) {
263
+ if (0 === refs.length) {
231
264
  return {
232
265
  step: index,
233
266
  tool: step.tool,
@@ -263,7 +296,7 @@ async function runTestidStep(session, step, index, value, dynamic, confirmDanger
263
296
  const expectTestid = step.expect?.element?.testid;
264
297
  if (expectTestid !== undefined && !dynamic.has(expectTestid)) {
265
298
  const expectRefs = await resolveTestid(session, expectTestid, sleep);
266
- if (expectRefs.refs.length === 0) {
299
+ if (0 === expectRefs.refs.length) {
267
300
  return {
268
301
  step: index,
269
302
  tool: step.tool,
@@ -278,27 +311,50 @@ async function runTestidStep(session, step, index, value, dynamic, confirmDanger
278
311
  return result;
279
312
  }
280
313
  /**
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.
314
+ * After a step's anchor resolves and its action runs, its `expect` is EVALUATED — every kind of it.
315
+ *
316
+ * For a long time only `expect.state` was, so a recorded `expect.signal` or `expect.net` was written
317
+ * to disk and read by nothing while `flow_save` graded the flow "asserted". Driven end to end over
318
+ * MCP: annotate a step with a signal that never fires, save (grade "asserted"), replay -> status
319
+ * "ok". A green that cannot go red, inside the feature whose job is catching exactly that.
320
+ *
321
+ * It compiles through the SAME `successToPredicate` the flow-level `success` has always used, so
322
+ * there is one definition of what an expect means and the step form cannot drift from it again.
323
+ * Turning this on makes previously-green flows go red. That is the point — they were green because
324
+ * nothing was looking.
285
325
  */
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;
326
+ async function assertStepExpect(session, expect, dynamic, waitForSignal, timeoutMs, since) {
327
+ // `element` is deliberately dropped: the step runner already asserts expect.element.testid against
328
+ // the live DOM before we get here, and re-asserting it through the predicate engine would be a
329
+ // second round trip for the same fact.
330
+ const { element: _element, ...consequences } = expect;
331
+ const predicate = successToPredicate(consequences, dynamic);
332
+ if (predicate === undefined)
333
+ return undefined;
292
334
  const verdict = await waitForSignal(session, predicate, timeoutMs, since);
293
335
  if (verdict.pass)
294
336
  return undefined;
295
337
  return {
296
- reasonKind: DriftReason.STATE_MISMATCH,
297
- reason: verdict.failureReason ?? `state '${state.path}' did not hold`,
298
- anchor: `state:${state.path}`,
338
+ // The store case keeps its own kind because heal and the run report branch on it; everything
339
+ // else is a consequence that did not hold, and the reason carries observed-vs-expected.
340
+ reasonKind: expect.state !== undefined ? DriftReason.STATE_MISMATCH : DriftReason.SIGNAL_NOT_OBSERVED,
341
+ reason: verdict.failureReason ?? "the step's declared consequence did not hold",
342
+ anchor: expectLabel(expect),
299
343
  nearest: null,
300
344
  };
301
345
  }
346
+ /** Name the thing that was asserted, for the drift's `anchor` column. */
347
+ function expectLabel(expect) {
348
+ if (expect.signal !== undefined)
349
+ return `signal:${expect.signal}`;
350
+ if (expect.net !== undefined)
351
+ return `net:${expect.net.urlContains ?? expect.net.method ?? '*'}`;
352
+ if (expect.state !== undefined)
353
+ return `state:${expect.state.path}`;
354
+ if (expect.console !== undefined)
355
+ return `console:${expect.console.level ?? '*'}`;
356
+ return 'expect';
357
+ }
302
358
  /** Run one signal-anchored step: wait for the signal predicate, else drift (no nearest for signals). */
303
359
  async function runSignalStep(session, step, index, name, waitForSignal, signalTimeoutMs, since) {
304
360
  // Scope to THIS replay's floor, not the whole buffer.
@@ -310,7 +366,7 @@ async function runSignalStep(session, step, index, name, waitForSignal, signalTi
310
366
  // satisfied a LATER flow's signal step even when that flow's own action never fired it — a
311
367
  // cross-flow false green on the exact suite-verify path the regression-cost claim rests on. The
312
368
  // 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);
369
+ const verdict = await waitForSignal(session, { kind: PredicateKind.SIGNAL, name }, signalTimeoutMs, since);
314
370
  if (verdict.pass)
315
371
  return { step: index, tool: step.tool, anchor: name, ok: true };
316
372
  return {
@@ -348,28 +404,39 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
348
404
  const page = currentRoute(session);
349
405
  // Event-time floor so the consequence reflects only THIS step's aftermath, not prior steps'.
350
406
  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) {
407
+ const subSteps = step.steps;
408
+ // Traced per step, so a slow replay says WHICH step and which anchor kind spent the time. The
409
+ // per-step `durationMs` below is what the agent gets back; this is what a developer profiling
410
+ // the replay engine gets, nested under the tool call with the browser round-trips beneath it.
411
+ const result = await span('flow.step', { index, anchor: step.anchor.kind, label }, async () => {
412
+ if (isDegradedAnchor(step.anchor)) {
413
+ // Never QUERY the sentinel — it marks "no anchor was determined", not an element to find.
414
+ return degradedStepResult(step, index, label);
415
+ }
416
+ if (subSteps !== undefined && subSteps.length > 0) {
417
+ return runSequenceStep(session, step, index, subSteps, confirmDangerous, sleep);
418
+ }
419
+ if (step.anchor.kind === AnchorKind.SIGNAL) {
420
+ return runSignalStep(session, step, index, label, waitForSignal, signalTimeoutMs, replayFloor);
421
+ }
422
+ if (step.anchor.kind === AnchorKind.COMPONENT) {
423
+ return runComponentStep(session, step, index, step.anchor, confirmDangerous, sleep);
424
+ }
425
+ if (step.anchor.kind === AnchorKind.ROLE && step.anchor.name !== undefined) {
426
+ // A NAMED role anchor addresses one element. The nameless one is the degraded placeholder
427
+ // and keeps its old path, where it fails legibly rather than querying a role as a testid.
428
+ return runRoleStep(session, step, index, step.anchor, confirmDangerous, sleep);
429
+ }
430
+ return runTestidStep(session, step, index, label, dynamic, confirmDangerous, sleep);
431
+ });
432
+ // Once the anchor resolved and the action ran, the step's own expect is evaluated — signal, net,
433
+ // console and store truth alike — deterministically, in the same cheap replay loop, with no LLM.
434
+ const stepExpect = step.expect;
435
+ if (result.ok && result.drift === undefined && stepExpect !== undefined) {
436
+ const expectDrift = await assertStepExpect(session, stepExpect, dynamic, waitForSignal, signalTimeoutMs, cursorBefore);
437
+ if (expectDrift !== undefined) {
371
438
  result.ok = false;
372
- result.drift = stateDrift;
439
+ result.drift = expectDrift;
373
440
  }
374
441
  }
375
442
  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>;