@reticlehq/server 3.4.0 → 3.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 (350) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +16 -8
  3. package/dist/chromium-launch-options.d.ts +3 -1
  4. package/dist/chromium-launch-options.js +2 -1
  5. package/dist/command/cli/adhoc-verdict.d.ts +1 -0
  6. package/dist/command/cli/adhoc-verdict.js +6 -3
  7. package/dist/command/cli/answers/open-note.d.ts +2 -0
  8. package/dist/command/cli/answers/open-note.js +22 -0
  9. package/dist/command/cli/auth/cloud-session.d.ts +4 -4
  10. package/dist/command/cli/auth/device-flow.d.ts +2 -2
  11. package/dist/command/cli/cli-flow-commands.js +4 -8
  12. package/dist/command/cli/cli-parse.d.ts +1 -1
  13. package/dist/command/cli/cli-parse.js +13 -11
  14. package/dist/command/cli/cli-verify.d.ts +1 -1
  15. package/dist/command/cli/cli-verify.js +35 -25
  16. package/dist/command/cli/cloud-cli.js +110 -12
  17. package/dist/command/cli/cloud-login.d.ts +9 -1
  18. package/dist/command/cli/cloud-login.js +43 -8
  19. package/dist/command/cli/demo/demo-page.js +11 -3
  20. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +46 -0
  21. package/dist/command/cli/doctor/browser/chromium-hint.js +99 -1
  22. package/dist/command/cli/drive/drive-attach.d.ts +27 -2
  23. package/dist/command/cli/drive/drive-attach.js +71 -4
  24. package/dist/command/cli/drive/drive-command.d.ts +2 -1
  25. package/dist/command/cli/drive/drive-command.js +13 -7
  26. package/dist/command/cli/lifecycle/daemon-lifecycle.js +4 -1
  27. package/dist/command/cli/ports/port-holder.d.ts +10 -0
  28. package/dist/command/cli/ports/port-holder.js +24 -0
  29. package/dist/command/cli/setup-mcp-cli.js +15 -5
  30. package/dist/command/cli/tutorial.d.ts +2 -2
  31. package/dist/command/cli/tutorial.js +6 -9
  32. package/dist/command/cli.d.ts +6 -0
  33. package/dist/command/cli.js +45 -23
  34. package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
  35. package/dist/command/daemon/binding/daemon-status-probe.js +1 -0
  36. package/dist/command/daemon/daemon-resolve.d.ts +14 -0
  37. package/dist/command/daemon/daemon-resolve.js +33 -2
  38. package/dist/command/setup/agent-writer.d.ts +1 -1
  39. package/dist/command/setup/agent-writer.js +3 -2
  40. package/dist/command/setup/bringup/connect-lease.d.ts +1 -0
  41. package/dist/command/setup/bringup/connect-lease.js +1 -0
  42. package/dist/command/setup/bringup/dev-server-wait.d.ts +10 -0
  43. package/dist/command/setup/bringup/dev-server-wait.js +20 -1
  44. package/dist/command/setup/init/init-port.d.ts +4 -0
  45. package/dist/command/setup/init/init-port.js +10 -0
  46. package/dist/command/setup/init/init-runtime.d.ts +10 -0
  47. package/dist/command/setup/init/init-runtime.js +71 -9
  48. package/dist/command/setup/node-effects.js +8 -2
  49. package/dist/command/setup/remaining-steps.js +2 -1
  50. package/dist/command/setup/run-setup.d.ts +7 -0
  51. package/dist/command/setup/run-setup.js +38 -25
  52. package/dist/command/setup/setup-command.d.ts +2 -1
  53. package/dist/command/setup/setup-command.js +2 -0
  54. package/dist/command/setup/setup-mcp.d.ts +2 -0
  55. package/dist/command/setup/setup-mcp.js +14 -5
  56. package/dist/command/version/peer-announce.d.ts +6 -2
  57. package/dist/command/version/peer-announce.js +8 -6
  58. package/dist/command/version/version-nudge.d.ts +17 -3
  59. package/dist/command/version/version-nudge.js +25 -4
  60. package/dist/command/version/version-skew.js +11 -2
  61. package/dist/features/crawl/crawl-tools.js +4 -0
  62. package/dist/features/crawl/crawl.d.ts +6 -0
  63. package/dist/features/crawl/crawl.js +9 -1
  64. package/dist/features/exhaust/ledger.d.ts +7 -0
  65. package/dist/features/exhaust/ledger.js +35 -9
  66. package/dist/features/harness/app-findings.d.ts +2 -0
  67. package/dist/features/harness/app-findings.js +52 -0
  68. package/dist/features/harness/drive-report.d.ts +27 -1
  69. package/dist/features/harness/drive-report.js +162 -11
  70. package/dist/features/harness/drivers.d.ts +10 -14
  71. package/dist/features/harness/drivers.js +10 -14
  72. package/dist/features/harness/fill-values.d.ts +3 -69
  73. package/dist/features/harness/fill-values.js +3 -133
  74. package/dist/features/harness/goals.d.ts +21 -0
  75. package/dist/features/harness/goals.js +77 -0
  76. package/dist/features/harness/harness.d.ts +21 -0
  77. package/dist/features/harness/harness.js +69 -2
  78. package/dist/features/harness/platform/drive-target.d.ts +30 -0
  79. package/dist/features/harness/platform/drive-target.js +81 -0
  80. package/dist/features/harness/platform/local-apps.d.ts +94 -0
  81. package/dist/features/harness/platform/local-apps.js +204 -0
  82. package/dist/features/harness/platform/remote-drive.d.ts +180 -0
  83. package/dist/features/harness/platform/remote-drive.js +365 -0
  84. package/dist/features/harness/platform/script.d.ts +48 -0
  85. package/dist/features/harness/platform/script.js +128 -0
  86. package/dist/features/harness/platform/server-driver.d.ts +41 -0
  87. package/dist/features/harness/platform/server-driver.js +179 -0
  88. package/dist/features/harness/platform-config.d.ts +7 -0
  89. package/dist/features/harness/platform-config.js +14 -2
  90. package/dist/features/harness/script/app-controls.d.ts +5 -0
  91. package/dist/features/harness/script/app-controls.js +40 -0
  92. package/dist/features/harness/script/lane-ids.d.ts +14 -0
  93. package/dist/features/harness/script/lane-ids.js +16 -0
  94. package/dist/features/harness/script/script-run.d.ts +41 -0
  95. package/dist/features/harness/script/script-run.js +299 -0
  96. package/dist/features/license/license.d.ts +2 -2
  97. package/dist/features/visual/visual-tools.d.ts +10 -1
  98. package/dist/features/visual/visual-tools.js +40 -3
  99. package/dist/hooks/driven-by.d.ts +4 -0
  100. package/dist/hooks/driven-by.js +16 -0
  101. package/dist/hooks/hook-commands.js +15 -4
  102. package/dist/hooks/tool-log.d.ts +9 -0
  103. package/dist/hooks/tool-log.js +29 -0
  104. package/dist/index.d.ts +1 -0
  105. package/dist/index.js +91 -89
  106. package/dist/judgement/capsule/capsule-store.d.ts +4 -4
  107. package/dist/judgement/runs/artifact/build-verification-run.js +6 -0
  108. package/dist/judgement/runs/artifact/replay-mapping.d.ts +1 -1
  109. package/dist/judgement/runs/artifact/replay-mapping.js +16 -6
  110. package/dist/judgement/runs/artifact/run-store.d.ts +1 -1
  111. package/dist/judgement/runs/artifact/run-store.js +26 -1
  112. package/dist/judgement/runs/drive-run.d.ts +22 -0
  113. package/dist/judgement/runs/drive-run.js +99 -1
  114. package/dist/judgement/runs/repair-prompt.js +1 -1
  115. package/dist/judgement/runs/reticle-runner.js +8 -1
  116. package/dist/judgement/runs/verification-sync.d.ts +3 -2
  117. package/dist/judgement/runs/verification-sync.js +4 -3
  118. package/dist/language/flows/change/gate.d.ts +10 -0
  119. package/dist/language/flows/change/gate.js +22 -0
  120. package/dist/language/flows/decision.js +34 -0
  121. package/dist/language/flows/flow-anchor.d.ts +54 -8
  122. package/dist/language/flows/flow-anchor.js +88 -3
  123. package/dist/language/flows/flow-cross-step.d.ts +15 -0
  124. package/dist/language/flows/flow-cross-step.js +21 -0
  125. package/dist/language/flows/flow-learning.js +8 -11
  126. package/dist/language/flows/flow-mutate-tools.js +5 -3
  127. package/dist/language/flows/flow-replay-run.d.ts +23 -9
  128. package/dist/language/flows/flow-replay-run.js +101 -94
  129. package/dist/language/flows/flow-replay.d.ts +23 -1
  130. package/dist/language/flows/flow-replay.js +92 -19
  131. package/dist/language/flows/flow-scope.js +5 -1
  132. package/dist/language/flows/flow-step-runners.d.ts +20 -2
  133. package/dist/language/flows/flow-step-runners.js +236 -42
  134. package/dist/language/flows/flow-store-for-session.d.ts +2 -2
  135. package/dist/language/flows/flow-store-for-session.js +2 -2
  136. package/dist/language/flows/flow-tools.js +58 -34
  137. package/dist/language/flows/flows.js +5 -2
  138. package/dist/language/flows/heal-run.js +6 -4
  139. package/dist/language/flows/recording/flake.d.ts +2 -2
  140. package/dist/language/flows/recording/tape/recordings.d.ts +30 -4
  141. package/dist/language/flows/recording/tape/recordings.js +40 -8
  142. package/dist/language/flows/replay-from-hud.d.ts +20 -0
  143. package/dist/language/flows/replay-from-hud.js +51 -0
  144. package/dist/language/flows/replay.d.ts +9 -5
  145. package/dist/language/flows/replay.js +27 -9
  146. package/dist/language/flows/suite/suite-flakes.js +2 -0
  147. package/dist/language/flows/suite-selection.d.ts +2 -15
  148. package/dist/language/flows/suite-selection.js +4 -3
  149. package/dist/language/flows/verify-change-tools.js +8 -4
  150. package/dist/launch-chromium.d.ts +27 -5
  151. package/dist/launch-chromium.js +38 -8
  152. package/dist/memory/cloud/cloud-config.d.ts +12 -0
  153. package/dist/memory/cloud/cloud-config.js +15 -0
  154. package/dist/memory/cloud/cloud-sync.d.ts +17 -14
  155. package/dist/memory/cloud/cloud-sync.js +12 -1
  156. package/dist/memory/cloud/harness-config.d.ts +26 -1
  157. package/dist/memory/cloud/harness-config.js +99 -4
  158. package/dist/memory/cloud/hud-notices-source.d.ts +21 -0
  159. package/dist/memory/cloud/hud-notices-source.js +128 -0
  160. package/dist/memory/cloud/memory-scope.d.ts +144 -0
  161. package/dist/memory/cloud/memory-scope.js +205 -0
  162. package/dist/memory/cloud/sync-cycle.d.ts +21 -3
  163. package/dist/memory/cloud/sync-cycle.js +45 -5
  164. package/dist/memory/cloud/sync-daemon.js +7 -5
  165. package/dist/memory/cloud/sync-disk.d.ts +1 -1
  166. package/dist/memory/cloud/sync-disk.js +5 -8
  167. package/dist/memory/impact/account-watch.d.ts +19 -0
  168. package/dist/memory/impact/account-watch.js +34 -0
  169. package/dist/memory/impact/impact-recorder.d.ts +8 -0
  170. package/dist/memory/impact/impact-recorder.js +12 -2
  171. package/dist/memory/impact/impact-store.d.ts +16 -0
  172. package/dist/memory/impact/impact-store.js +142 -26
  173. package/dist/memory/intent/intent-shard.d.ts +2 -2
  174. package/dist/memory/intent/intent-tools.js +88 -5
  175. package/dist/memory/journal/envelope.d.ts +16 -16
  176. package/dist/memory/journal/journal-query.d.ts +14 -0
  177. package/dist/memory/journal/journal-query.js +15 -0
  178. package/dist/memory/journal/journal-recorder.js +7 -1
  179. package/dist/memory/journal/on-disk/retention.d.ts +3 -0
  180. package/dist/memory/journal/on-disk/retention.js +5 -0
  181. package/dist/memory/journal/on-disk/startup-maintenance.js +3 -1
  182. package/dist/memory/journal/on-disk/workspace-tiers.js +7 -4
  183. package/dist/memory/journal/session-end.js +6 -4
  184. package/dist/memory/project/artifact-root-resolver.d.ts +36 -1
  185. package/dist/memory/project/artifact-root-resolver.js +134 -26
  186. package/dist/memory/project/artifact-root.d.ts +42 -5
  187. package/dist/memory/project/artifact-root.js +87 -8
  188. package/dist/memory/project/dir/reticle-dir.d.ts +4 -2
  189. package/dist/memory/project/dir/reticle-dir.js +3 -1
  190. package/dist/memory/project/project-tools.js +22 -3
  191. package/dist/memory/project/session-root.d.ts +24 -1
  192. package/dist/memory/project/session-root.js +41 -5
  193. package/dist/memory/project/sync-status.d.ts +39 -0
  194. package/dist/memory/project/sync-status.js +90 -0
  195. package/dist/memory/recall/memory-tools.js +2 -0
  196. package/dist/memory/recall/project-memory.d.ts +19 -0
  197. package/dist/memory/recall/project-memory.js +45 -6
  198. package/dist/portal/bridge/bridge.d.ts +7 -4
  199. package/dist/portal/bridge/bridge.js +25 -3
  200. package/dist/portal/bridge/pairing-token.d.ts +9 -0
  201. package/dist/portal/bridge/pairing-token.js +17 -0
  202. package/dist/portal/input/real-input.d.ts +81 -10
  203. package/dist/portal/input/real-input.js +207 -22
  204. package/dist/portal/input/viewport-tools.js +19 -9
  205. package/dist/portal/pool/browser-pool.d.ts +22 -9
  206. package/dist/portal/pool/browser-pool.js +70 -35
  207. package/dist/portal/pool/playwright-launcher.d.ts +13 -0
  208. package/dist/portal/pool/playwright-launcher.js +17 -1
  209. package/dist/portal/pool/pool-contract.d.ts +7 -4
  210. package/dist/portal/session/command-payload.d.ts +8 -0
  211. package/dist/portal/session/command-payload.js +19 -0
  212. package/dist/portal/session/dev-server/dev-command.d.ts +12 -0
  213. package/dist/portal/session/dev-server/dev-command.js +61 -12
  214. package/dist/portal/session/facts/departure-note.d.ts +22 -0
  215. package/dist/portal/session/facts/departure-note.js +47 -0
  216. package/dist/portal/session/facts/session-replaced.d.ts +5 -0
  217. package/dist/portal/session/facts/session-replaced.js +5 -0
  218. package/dist/portal/session/first-run-wiring.d.ts +42 -0
  219. package/dist/portal/session/first-run-wiring.js +132 -0
  220. package/dist/portal/session/human/notes-ledger.d.ts +20 -0
  221. package/dist/portal/session/human/notes-ledger.js +55 -0
  222. package/dist/portal/session/human/review-store.d.ts +12 -0
  223. package/dist/portal/session/human/review-store.js +18 -10
  224. package/dist/portal/session/live-control-tools.d.ts +0 -13
  225. package/dist/portal/session/live-control-tools.js +28 -3
  226. package/dist/portal/session/no-session-diagnosis.d.ts +7 -0
  227. package/dist/portal/session/no-session-diagnosis.js +15 -0
  228. package/dist/portal/session/no-session-next-action.d.ts +11 -0
  229. package/dist/portal/session/no-session-next-action.js +31 -5
  230. package/dist/portal/session/no-session-watch.d.ts +14 -1
  231. package/dist/portal/session/no-session-watch.js +63 -10
  232. package/dist/portal/session/presence/session-mirror.js +3 -0
  233. package/dist/portal/session/serving-directory.d.ts +27 -0
  234. package/dist/portal/session/serving-directory.js +45 -0
  235. package/dist/portal/session/session-health.d.ts +7 -0
  236. package/dist/portal/session/session-health.js +6 -0
  237. package/dist/portal/session/session-info.d.ts +8 -1
  238. package/dist/portal/session/session-info.js +6 -0
  239. package/dist/portal/session/session-manager.d.ts +21 -0
  240. package/dist/portal/session/session-manager.js +44 -8
  241. package/dist/portal/session/session-reaper.js +4 -1
  242. package/dist/portal/session/session-successor.d.ts +5 -0
  243. package/dist/portal/session/session-tools.js +26 -3
  244. package/dist/portal/session/session.d.ts +12 -10
  245. package/dist/portal/session/session.js +30 -31
  246. package/dist/start-options.d.ts +6 -0
  247. package/dist/status-payload.d.ts +10 -0
  248. package/dist/status-payload.js +23 -0
  249. package/dist/surface/http-server.d.ts +1 -1
  250. package/dist/surface/http-server.js +15 -4
  251. package/dist/surface/mcp/mcp-proxy.d.ts +5 -0
  252. package/dist/surface/mcp/mcp-proxy.js +36 -5
  253. package/dist/surface/mcp/mcp.d.ts +11 -6
  254. package/dist/surface/mcp/mcp.js +28 -14
  255. package/dist/surface/mcp/proxy/proxy-log.js +3 -5
  256. package/dist/surface/mcp/server-instructions.d.ts +4 -2
  257. package/dist/surface/mcp/server-instructions.js +14 -4
  258. package/dist/surface/mcp/shared-params.js +2 -2
  259. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +8 -0
  260. package/dist/surface/tools/act/navigation/navigate-arrival.js +56 -2
  261. package/dist/surface/tools/act/settle-in-flight.js +25 -3
  262. package/dist/surface/tools/act-sequence-tool.js +2 -2
  263. package/dist/surface/tools/act-tools.js +25 -9
  264. package/dist/surface/tools/assert/assert-verdict.d.ts +1 -1
  265. package/dist/surface/tools/assert/assert-verdict.js +17 -2
  266. package/dist/surface/tools/browser-tools.js +8 -1
  267. package/dist/surface/tools/chat-drives.d.ts +26 -0
  268. package/dist/surface/tools/chat-drives.js +165 -0
  269. package/dist/surface/tools/dynamic-tools.js +29 -2
  270. package/dist/surface/tools/empty-session-list.d.ts +41 -0
  271. package/dist/surface/tools/empty-session-list.js +80 -0
  272. package/dist/surface/tools/error-recovery.d.ts +6 -0
  273. package/dist/surface/tools/error-recovery.js +38 -6
  274. package/dist/surface/tools/explore-tools.d.ts +10 -1
  275. package/dist/surface/tools/explore-tools.js +82 -13
  276. package/dist/surface/tools/feedback-tools.js +14 -0
  277. package/dist/surface/tools/harness-explore.d.ts +96 -14
  278. package/dist/surface/tools/harness-explore.js +227 -202
  279. package/dist/surface/tools/harness-plan.d.ts +12 -0
  280. package/dist/surface/tools/harness-plan.js +31 -0
  281. package/dist/surface/tools/harness-script.d.ts +8 -0
  282. package/dist/surface/tools/harness-script.js +342 -0
  283. package/dist/surface/tools/harness-toolset.d.ts +14 -1
  284. package/dist/surface/tools/harness-toolset.js +77 -6
  285. package/dist/surface/tools/invoke-tool.js +64 -9
  286. package/dist/surface/tools/lease-seed.d.ts +29 -0
  287. package/dist/surface/tools/lease-seed.js +123 -0
  288. package/dist/surface/tools/lease-tools.d.ts +3 -23
  289. package/dist/surface/tools/lease-tools.js +30 -131
  290. package/dist/surface/tools/live-call-text.js +2 -2
  291. package/dist/surface/tools/next-step.d.ts +19 -0
  292. package/dist/surface/tools/next-step.js +159 -0
  293. package/dist/surface/tools/observe-tools.js +2 -4
  294. package/dist/surface/tools/platform-moment.d.ts +12 -0
  295. package/dist/surface/tools/platform-moment.js +168 -0
  296. package/dist/surface/tools/query-hint-schema.d.ts +7 -0
  297. package/dist/surface/tools/query-hint-schema.js +10 -0
  298. package/dist/surface/tools/read-tools.js +8 -2
  299. package/dist/surface/tools/real-input-attempt.js +96 -13
  300. package/dist/surface/tools/tool-kit.d.ts +25 -1
  301. package/dist/surface/tools/tool-kit.js +4 -6
  302. package/dist/surface/tools/tools.js +20 -48
  303. package/dist/telemetry/argument-shape.js +1 -1
  304. package/dist/telemetry/hud-metrics.d.ts +2 -0
  305. package/dist/telemetry/hud-metrics.js +17 -0
  306. package/dist/telemetry/onboarding-firsts.d.ts +2 -2
  307. package/dist/telemetry/onboarding-firsts.js +2 -2
  308. package/dist/telemetry/telemetry.d.ts +12 -0
  309. package/dist/telemetry/telemetry.js +43 -14
  310. package/dist/telemetry/verification-of.js +1 -0
  311. package/dist/wire-journal.js +4 -1
  312. package/docs/README.md +1 -1
  313. package/docs/actions.mdx +1 -2
  314. package/docs/agent-cheatsheet.md +1 -1
  315. package/docs/autodrive.md +29 -25
  316. package/docs/cli/cloud.mdx +6 -4
  317. package/docs/cli/doctor.mdx +6 -0
  318. package/docs/cli/drive.mdx +8 -2
  319. package/docs/cli/init.mdx +11 -13
  320. package/docs/cli/status.mdx +5 -1
  321. package/docs/cli/version.mdx +3 -3
  322. package/docs/cli.mdx +14 -8
  323. package/docs/desktop.mdx +5 -1
  324. package/docs/environment.mdx +3 -7
  325. package/docs/getting-started.md +14 -2
  326. package/docs/human-control.md +1 -2
  327. package/docs/index.mdx +4 -3
  328. package/docs/install-agentic.mdx +1 -1
  329. package/docs/install-manual.mdx +2 -2
  330. package/docs/packages/next.mdx +3 -0
  331. package/docs/packages/tauri.mdx +2 -2
  332. package/docs/packages/vite-plugin.mdx +24 -1
  333. package/docs/packages.mdx +1 -1
  334. package/docs/quickstart.mdx +23 -11
  335. package/docs/telemetry.md +1 -1
  336. package/docs/tools/act.mdx +1 -1
  337. package/docs/tools/session-and-feedback.mdx +9 -9
  338. package/docs/usage.md +5 -3
  339. package/docs/what-is-recorded.md +13 -3
  340. package/docs/what-is-reticle.mdx +3 -1
  341. package/package.json +8 -12
  342. package/dist/features/harness/driver.d.ts +0 -50
  343. package/dist/features/harness/driver.js +0 -236
  344. package/dist/features/harness/jev-driver.d.ts +0 -188
  345. package/dist/features/harness/jev-driver.js +0 -678
  346. package/dist/features/harness/openai-driver.d.ts +0 -46
  347. package/dist/features/harness/openai-driver.js +0 -194
  348. package/dist/memory/project/dir/fill-value-store.d.ts +0 -32
  349. package/dist/memory/project/dir/fill-value-store.js +0 -88
  350. package/docs/local-registry.md +0 -143
@@ -6,9 +6,13 @@
6
6
  */
7
7
  import { ReplayStatus, RUN_RECORDING_MAX_BYTES, RUN_STEP_RESULTS_MAX, RUN_TEXT_MAX, RunFlowStatus, } from '@reticlehq/core';
8
8
  import { SUCCESS_STEP_TOOL } from '../../../language/flows/flow-success.js';
9
- /** OK → PASS; DRIFT/ERROR → FAIL (a healed flow is produced by the heal path, not plain replay). */
9
+ /** OK → PASS; UNVERIFIABLE → SKIPPED; DRIFT/ERROR → FAIL (a healed flow is produced by the heal path, not plain replay). */
10
10
  export function runFlowStatusOf(status) {
11
- return status === ReplayStatus.OK ? RunFlowStatus.PASS : RunFlowStatus.FAIL;
11
+ if (status === ReplayStatus.OK)
12
+ return RunFlowStatus.PASS;
13
+ if (status === ReplayStatus.UNVERIFIABLE)
14
+ return RunFlowStatus.SKIPPED;
15
+ return RunFlowStatus.FAIL;
12
16
  }
13
17
  const clip = (text) => text.length > RUN_TEXT_MAX ? `${text.slice(0, RUN_TEXT_MAX - 1)}…` : text;
14
18
  /** One step as synced: what it did, where, and whether it held. */
@@ -39,9 +43,12 @@ function recordingOf(flow) {
39
43
  }
40
44
  /** Map one replay (plus its measured duration) into a RunFlowResult for the artifact's flows[]. */
41
45
  export function mapReplayToFlowResult(replay, durationMs, flow) {
42
- // A replay that could not start returns OK with no steps. Nothing ran, so it is SKIPPED — which
43
- // every reader of a run (the verdict, the gate) already refuses to count as a pass.
44
- const status = replay.unverifiable === undefined ? runFlowStatusOf(replay.status) : RunFlowStatus.SKIPPED;
46
+ // A replay that could not start or complete returns UNVERIFIABLE (or carries an unverifiable caveat).
47
+ // Nothing was verified, so it is SKIPPED — which every reader of a run (the verdict, the gate)
48
+ // refuses to count as a pass.
49
+ const status = replay.status === ReplayStatus.UNVERIFIABLE || replay.unverifiable !== undefined
50
+ ? RunFlowStatus.SKIPPED
51
+ : runFlowStatusOf(replay.status);
45
52
  const failureReason = replay.unverifiable !== undefined
46
53
  ? replay.unverifiable.reason
47
54
  : status === RunFlowStatus.FAIL
@@ -49,13 +56,16 @@ export function mapReplayToFlowResult(replay, durationMs, flow) {
49
56
  replay.decision?.summary ??
50
57
  replay.error?.message ??
51
58
  'flow failed')
52
- : undefined;
59
+ : status === RunFlowStatus.SKIPPED
60
+ ? 'flow could not be graded'
61
+ : undefined;
53
62
  // A flow with a success oracle gets a synthetic 'success' step appended by replay; surface its label
54
63
  // as the run's `oracle` so the verdict counts this flow as consequence-backed (→ HIGH confidence),
55
64
  // not a bare smoke click. Without this, an oracle-backed pass reads as MEDIUM and undersells itself.
56
65
  const oracle = replay.steps.find((s) => s.tool === SUCCESS_STEP_TOOL)?.anchor;
57
66
  return {
58
67
  name: replay.name,
68
+ ...(flow?.template === undefined ? {} : { template: flow.template }),
59
69
  status,
60
70
  steps: replay.steps.length,
61
71
  durationMs,
@@ -38,7 +38,7 @@ export declare class RunStore {
38
38
  * Guards runId as a safe path segment BEFORE building the path — a runId can originate from a caller
39
39
  * (an OEM may set it), so an unsafe value must never escape .reticle/runs/ (mirrors the read guard).
40
40
  */
41
- write(run: ReticleVerificationRun): Promise<void>;
41
+ write(given: ReticleVerificationRun): Promise<void>;
42
42
  /** Never throws. Invalid id / missing → MISSING; bad JSON or failed schema → MALFORMED. */
43
43
  read(runId: RunId): Promise<ReadRunResult>;
44
44
  /** List run ids on disk (filenames minus.json). Empty when the runs dir is absent. */
@@ -6,10 +6,13 @@
6
6
  * needs no clock.
7
7
  */
8
8
  import { asRunId, ReticleVerificationRunSchema, RUN_RETENTION, RUN_RETENTION_SLACK, RunReadError, } from '@reticlehq/core';
9
+ import { PromptContextSchema } from '@reticlehq/core/artifacts';
9
10
  import { reticleDirPaths, isValidRunId, runPath } from '../../../memory/project/dir/reticle-dir.js';
10
11
  const JSON_INDENT = 2;
11
12
  const JSON_EXT = '.json';
12
13
  const TMP_EXT = '.tmp';
14
+ /** A request older than this is about some other task, not the run being written now. */
15
+ const REQUEST_RELEVANT_MS = 6 * 60 * 60 * 1000;
13
16
  export class RunStore {
14
17
  #fs;
15
18
  #root;
@@ -28,11 +31,15 @@ export class RunStore {
28
31
  * Guards runId as a safe path segment BEFORE building the path — a runId can originate from a caller
29
32
  * (an OEM may set it), so an unsafe value must never escape .reticle/runs/ (mirrors the read guard).
30
33
  */
31
- async write(run) {
34
+ async write(given) {
35
+ let run = given;
32
36
  if (!isValidRunId(run.runId)) {
33
37
  throw new Error(`refusing to write run with unsafe runId: ${JSON.stringify(run.runId)}`);
34
38
  }
35
39
  await this.#fs.mkdir(reticleDirPaths(this.#root).runs);
40
+ const context = run.context ?? (await this.#recentRequest(run.createdAt));
41
+ if (context !== undefined)
42
+ run = { ...run, context };
36
43
  // Atomic publish: write a temp file then rename, so a crash mid-write never leaves a half-written
37
44
  // artifact (a partial.json would otherwise read back as MALFORMED).
38
45
  const path = runPath(this.#root, run.runId);
@@ -49,6 +56,24 @@ export class RunStore {
49
56
  }
50
57
  await this.#pruneOld();
51
58
  }
59
+ /**
60
+ * The user's request this run answers, when the agent relayed one recently enough to be about it.
61
+ * Read from .reticle/intent/request.json (written by `reticle_intent { declare, request }`).
62
+ */
63
+ async #recentRequest(at) {
64
+ try {
65
+ const parsed = PromptContextSchema.safeParse(JSON.parse(await this.#fs.readFile(reticleDirPaths(this.#root).request)));
66
+ if (!parsed.success)
67
+ return undefined;
68
+ const given = parsed.data.at;
69
+ return given !== undefined && 0 <= at - given && at - given <= REQUEST_RELEVANT_MS
70
+ ? parsed.data
71
+ : undefined;
72
+ }
73
+ catch {
74
+ return undefined; // no request recorded: the common case
75
+ }
76
+ }
52
77
  /**
53
78
  * Keep .reticle/runs/ bounded. Only acts once the count exceeds RUN_RETENTION + SLACK, then deletes the
54
79
  * oldest (by createdAt) back down to RUN_RETENTION — so the read-all is amortized, not per-write.
@@ -59,3 +59,25 @@ export declare function driveRunId(sessionId: string): string;
59
59
  * artifact are the caller's job, which is what keeps the verdict rules testable without a disk.
60
60
  */
61
61
  export declare function driveRunFrom(actions: readonly JournalAction[], deps: DriveRunDeps): VerificationRunInput | undefined;
62
+ /** What a Harness run says drove it, in the dashboard's "Driven by" line. */
63
+ export declare function harnessAgentId(driver: string): string;
64
+ /**
65
+ * The id of the run a Harness drive syncs as, so whoever started the drive can find it again (the
66
+ * platform's chat folds the run into the check it asked for). Undefined for an id no run can carry.
67
+ */
68
+ export declare function harnessRunId(harness: string): string | undefined;
69
+ /**
70
+ * What the Harness judged of a drive's goal, noted the moment the drive finishes, before its
71
+ * session ends and is graded. A drive whose checks held but whose goal was not reached synced as
72
+ * "Proved" while the drive itself said it had failed. Unmet once is unmet: a later journey of the
73
+ * same drive reaching its goal does not undo it.
74
+ */
75
+ export declare function noteHarnessGoal(harness: string, met: boolean | undefined): void;
76
+ /**
77
+ * Every run a session's journal holds: the connected agent's drive, and one per Harness drive.
78
+ *
79
+ * They used to be one. A Harness drive dispatches through the same tools in the same tab, so its
80
+ * verdicts folded into the agent's run, and the dashboard showed one blended row that credited the
81
+ * agent with what the Harness proved and named neither.
82
+ */
83
+ export declare function driveRunsFrom(actions: readonly JournalAction[], deps: DriveRunDeps): VerificationRunInput[];
@@ -24,7 +24,7 @@
24
24
  */
25
25
  import { isValidRunId } from '../../memory/project/dir/reticle-dir.js';
26
26
  import { defaultRunId } from './runner-port.js';
27
- import { JournalVerdictEffectSchema, RunAgentKind, RunFramework, RunProfile, RunTrigger, } from '@reticlehq/core';
27
+ import { JournalVerdictEffectSchema, RunAgentKind, RunFramework, RunProfile, RunTrigger, RunFlowStatus, Verified, } from '@reticlehq/core';
28
28
  import { withCorrections } from './late-answer.js';
29
29
  /** The author of record when no MCP peer introduced itself. Mirrors verification-sync's default. */
30
30
  const UNNAMED_AGENT = 'reticle-mcp';
@@ -133,3 +133,101 @@ export function driveRunFrom(actions, deps) {
133
133
  evidence: { consoleErrors: [], networkAnomalies: [], stateAssertions: [], timeline: [] },
134
134
  };
135
135
  }
136
+ /** Marks a run as one Harness drive, so it never collides with the session's own `drive-` run. */
137
+ const HARNESS_RUN_PREFIX = 'harness-';
138
+ /** What a Harness run says drove it, in the dashboard's "Driven by" line. */
139
+ export function harnessAgentId(driver) {
140
+ return 'server' === driver ? 'Reticle Harness (platform)' : `Reticle Harness (${driver})`;
141
+ }
142
+ /**
143
+ * The id of the run a Harness drive syncs as, so whoever started the drive can find it again (the
144
+ * platform's chat folds the run into the check it asked for). Undefined for an id no run can carry.
145
+ */
146
+ export function harnessRunId(harness) {
147
+ const candidate = `${HARNESS_RUN_PREFIX}${harness}`;
148
+ return isValidRunId(candidate) ? candidate : undefined;
149
+ }
150
+ /** The journey a Harness drive was named for, or what it did when nobody named one. */
151
+ const UNNAMED_JOURNEY = 'Harness drive';
152
+ /** Goal verdicts kept for the drives a session has not yet been graded on. */
153
+ const MAX_GOALS_HELD = 200;
154
+ const goalsMissed = new Map();
155
+ /**
156
+ * What the Harness judged of a drive's goal, noted the moment the drive finishes, before its
157
+ * session ends and is graded. A drive whose checks held but whose goal was not reached synced as
158
+ * "Proved" while the drive itself said it had failed. Unmet once is unmet: a later journey of the
159
+ * same drive reaching its goal does not undo it.
160
+ */
161
+ export function noteHarnessGoal(harness, met) {
162
+ if (met === undefined)
163
+ return;
164
+ const before = goalsMissed.get(harness);
165
+ if (false === before)
166
+ return;
167
+ if (MAX_GOALS_HELD <= goalsMissed.size && !goalsMissed.has(harness)) {
168
+ const oldest = goalsMissed.keys().next().value;
169
+ if (oldest !== undefined)
170
+ goalsMissed.delete(oldest);
171
+ }
172
+ goalsMissed.set(harness, met);
173
+ }
174
+ /**
175
+ * One Harness drive, as its own run: named for the journey it was asked to complete, and credited to
176
+ * the Harness rather than to the agent whose tab it shared. The journey reads as one flow, failed
177
+ * when any check it declared came back no, passed when one came back yes and none failed.
178
+ */
179
+ function harnessRunFrom(by, actions, deps) {
180
+ const input = driveRunFrom(actions, {
181
+ ...deps,
182
+ runId: harnessRunId(by.harness) ?? defaultRunId(),
183
+ });
184
+ if (input === undefined)
185
+ return undefined;
186
+ const statuses = input.checks.map((check) => check.status);
187
+ const status = statuses.includes(Verified.NO) || false === goalsMissed.get(by.harness)
188
+ ? RunFlowStatus.FAIL
189
+ : statuses.includes(Verified.YES)
190
+ ? RunFlowStatus.PASS
191
+ : RunFlowStatus.SKIPPED;
192
+ const first = actions.reduce((min, action) => Math.min(min, action.at), Number.MAX_SAFE_INTEGER);
193
+ return {
194
+ ...input,
195
+ durationMs: Math.max(0, input.durationMs - first),
196
+ agent: { id: harnessAgentId(by.driver), kind: RunAgentKind.CODING_AGENT },
197
+ trigger: {
198
+ kind: RunTrigger.EDIT,
199
+ note: `driven by the Harness${by.persona === undefined ? '' : `: ${by.persona}`}`,
200
+ },
201
+ flows: [
202
+ {
203
+ name: by.persona ?? UNNAMED_JOURNEY,
204
+ status,
205
+ steps: actions.length,
206
+ durationMs: Math.max(0, input.durationMs - first),
207
+ },
208
+ ],
209
+ };
210
+ }
211
+ /**
212
+ * Every run a session's journal holds: the connected agent's drive, and one per Harness drive.
213
+ *
214
+ * They used to be one. A Harness drive dispatches through the same tools in the same tab, so its
215
+ * verdicts folded into the agent's run, and the dashboard showed one blended row that credited the
216
+ * agent with what the Harness proved and named neither.
217
+ */
218
+ export function driveRunsFrom(actions, deps) {
219
+ const own = actions.filter((action) => action.drivenBy === undefined);
220
+ const drives = new Map();
221
+ for (const action of actions) {
222
+ const by = action.drivenBy;
223
+ if (by === undefined)
224
+ continue;
225
+ const held = drives.get(by.harness) ?? { by, actions: [] };
226
+ held.actions.push(action);
227
+ drives.set(by.harness, held);
228
+ }
229
+ return [
230
+ driveRunFrom(own, deps),
231
+ ...[...drives.values()].map((drive) => harnessRunFrom(drive.by, drive.actions, deps)),
232
+ ].filter((run) => run !== undefined);
233
+ }
@@ -46,7 +46,7 @@ function deviationHeadline(deviation) {
46
46
  return undefined;
47
47
  }
48
48
  export function buildRepairPacket(replay) {
49
- if (replay.status === ReplayStatus.OK)
49
+ if (replay.status === ReplayStatus.OK || replay.status === ReplayStatus.UNVERIFIABLE)
50
50
  return undefined;
51
51
  const decision = replay.decision;
52
52
  const baseActual = decision?.whatChanged ?? replay.error?.message ?? 'the flow failed';
@@ -85,7 +85,14 @@ export class ReticleRunner {
85
85
  flows,
86
86
  checks: [],
87
87
  risks,
88
- evidence: { consoleErrors: [], networkAnomalies: [], stateAssertions: [], timeline: [] },
88
+ // What the replay windows saw. An empty list written here regardless read as a clean
89
+ // console on a run whose steps had counted errors (#1344).
90
+ evidence: {
91
+ consoleErrors: replays.flatMap((r) => r.steps.flatMap((step) => step.consoleErrors ?? [])),
92
+ networkAnomalies: [],
93
+ stateAssertions: [],
94
+ timeline: [],
95
+ },
89
96
  ...(failurePackets.length > 0 ? { repair: { failurePackets } } : {}),
90
97
  };
91
98
  const run = buildVerificationRun(input, () => this.#port.now());
@@ -1,4 +1,5 @@
1
- import { type ProjectId, type FlowFile, type FlowReplayResult } from '@reticlehq/core';
1
+ import { type FlowFile, type FlowReplayResult } from '@reticlehq/core';
2
+ import { type ProjectTarget } from '../../memory/project/session-root.js';
2
3
  import type { ToolDeps } from '../../surface/tools/tool-kit.js';
3
4
  /** A replay plus the wall-clock time it took — the shape the verify handler already collects. */
4
5
  export interface TimedReplay {
@@ -12,4 +13,4 @@ export interface TimedReplay {
12
13
  * tool — a disk or network failure is logged and swallowed so the verdict the agent asked for is
13
14
  * unaffected. Returns the runId (for logging/tests); undefined if nothing was produced (empty suite).
14
15
  */
15
- export declare function persistAndSyncVerificationRun(deps: ToolDeps, timed: TimedReplay[], projectId: ProjectId | undefined): Promise<string | undefined>;
16
+ export declare function persistAndSyncVerificationRun(deps: ToolDeps, timed: TimedReplay[], target: ProjectTarget): Promise<string | undefined>;
@@ -18,7 +18,7 @@ import { RunStore } from './artifact/run-store.js';
18
18
  import { cloudFetch, syncRunToCloud, SyncOutcome } from '../../memory/cloud/cloud-sync.js';
19
19
  import { resolveProjectCloud } from '../../memory/cloud/cloud-config.js';
20
20
  import { log } from '../../log.js';
21
- import { rootForProjectId } from '../../memory/project/session-root.js';
21
+ import { rootForTarget } from '../../memory/project/session-root.js';
22
22
  /** The author of record when no MCP peer introduced itself — a CLI run, or a client that skipped the handshake. */
23
23
  const RETICLE_MCP_AGENT_ID = 'reticle-mcp';
24
24
  /** Assemble the ReticleVerificationRun artifact for a verify suite (pure — no IO). */
@@ -57,7 +57,8 @@ function assembleRun(deps, timed, projectId) {
57
57
  * tool — a disk or network failure is logged and swallowed so the verdict the agent asked for is
58
58
  * unaffected. Returns the runId (for logging/tests); undefined if nothing was produced (empty suite).
59
59
  */
60
- export async function persistAndSyncVerificationRun(deps, timed, projectId) {
60
+ export async function persistAndSyncVerificationRun(deps, timed, target) {
61
+ const { projectId } = target;
61
62
  if (0 === timed.length)
62
63
  return undefined;
63
64
  let run;
@@ -66,7 +67,7 @@ export async function persistAndSyncVerificationRun(deps, timed, projectId) {
66
67
  // decides WHICH DASHBOARD this run is pushed to. The daemon's own directory has no link file, so
67
68
  // a misrouted read silently meant "not attached" — and where it DID have one, the run was pushed
68
69
  // to a dashboard belonging to whoever that checkout was linked to.
69
- const root = rootForProjectId(deps, projectId);
70
+ const root = rootForTarget(deps, target);
70
71
  try {
71
72
  run = assembleRun(deps, timed, projectId);
72
73
  await new RunStore(deps.fs, root).write(run);
@@ -5,6 +5,7 @@
5
5
  * unexplained flake that blocks a merge destroys trust in the red. Pure decision; the CLI computes the
6
6
  * inputs (git diff → affected → run artifacts + flake ledger) and maps `pass` to the exit code.
7
7
  */
8
+ import { type RunFlowResult } from '@reticlehq/core';
8
9
  /** A flow whose assertions were WEAKENED since its last passing run. */
9
10
  interface DowngradedFlow {
10
11
  flow: string;
@@ -56,4 +57,13 @@ interface GateResult {
56
57
  unexecuted: string[];
57
58
  }
58
59
  export declare function gateDecision(input: GateInput): GateResult;
60
+ /**
61
+ * The flows a run lets the gate count as passing.
62
+ *
63
+ * Every PASS or HEALED flow by its own name, as before. A generated copy of a template also credits
64
+ * the template, but only on a STRICT pass that proved a consequence (#1321): a suite replays runtime
65
+ * copies with fixture parameters and deletes them, so their own names match no saved flow. A healed
66
+ * anchor, a failed or unverifiable replay, and a pass that checked nothing credit nothing.
67
+ */
68
+ export declare function passingFlowNames(flows: readonly RunFlowResult[]): string[];
59
69
  export {};
@@ -5,6 +5,7 @@
5
5
  * unexplained flake that blocks a merge destroys trust in the red. Pure decision; the CLI computes the
6
6
  * inputs (git diff → affected → run artifacts + flake ledger) and maps `pass` to the exit code.
7
7
  */
8
+ import { RunFlowStatus } from '@reticlehq/core';
8
9
  export function gateDecision(input) {
9
10
  const passing = new Set(input.passing);
10
11
  const flaky = new Set(input.flaky ?? []);
@@ -42,3 +43,24 @@ export function gateDecision(input) {
42
43
  unexecuted,
43
44
  };
44
45
  }
46
+ /**
47
+ * The flows a run lets the gate count as passing.
48
+ *
49
+ * Every PASS or HEALED flow by its own name, as before. A generated copy of a template also credits
50
+ * the template, but only on a STRICT pass that proved a consequence (#1321): a suite replays runtime
51
+ * copies with fixture parameters and deletes them, so their own names match no saved flow. A healed
52
+ * anchor, a failed or unverifiable replay, and a pass that checked nothing credit nothing.
53
+ */
54
+ export function passingFlowNames(flows) {
55
+ const names = new Set();
56
+ for (const f of flows) {
57
+ if (f.status !== RunFlowStatus.PASS && f.status !== RunFlowStatus.HEALED)
58
+ continue;
59
+ names.add(f.name);
60
+ const proved = f.oracle !== undefined ||
61
+ (f.stepResults ?? []).some((s) => s.ok && s.consequence !== undefined);
62
+ if (f.template !== undefined && f.status === RunFlowStatus.PASS && proved)
63
+ names.add(f.template);
64
+ }
65
+ return [...names];
66
+ }
@@ -123,6 +123,35 @@ invoked = new Map()) {
123
123
  : 'the action ran and the expected consequence never appeared — check the handler behind it, or whether this flow needs a state it did not start in. The locator is not the problem.',
124
124
  };
125
125
  }
126
+ /*
127
+ * An anchor that matched SEVERAL elements is not an element that is gone, and the advice below
128
+ * says it is.
129
+ *
130
+ * `ANCHOR_AMBIGUOUS` carries `nearest: null` and `ambiguous: true`, so it fell through to the
131
+ * last line of this branch — "the anchored element is gone; rebind or update the flow" — for a
132
+ * locator that resolved several times over and lost nothing. Rebind TO WHAT is the question the
133
+ * drift just refused to answer; sending the reader looking for a replacement locator is the
134
+ * opposite of the fix, which is a NARROWER anchor on the one they already have.
135
+ *
136
+ * The same sentence was already wrong for a testid ambiguity (it is why `ambiguous: true` exists
137
+ * on that drift at all), and the role and component runners make it reachable for two more
138
+ * anchor kinds. The distinction this branch has to keep is the one it was written for: a
139
+ * CONSEQUENCE that never appeared has no anchor problem, an AMBIGUOUS anchor has nothing else.
140
+ */
141
+ if (DriftReason.ANCHOR_AMBIGUOUS === drift.reasonKind) {
142
+ return {
143
+ verdict: 'drift',
144
+ summary: withIntent(`"${name}" drifted at step ${step.step} (${step.anchor}) — the anchor is ambiguous.`, intentSaid),
145
+ whatChanged: drift.reason,
146
+ // The step's OWN anchor is the thing at fault here, so its source file is the right place to
147
+ // look — unlike the consequence case above, where the recorded source belongs to the element
148
+ // that was acted on and the anchor is not what failed.
149
+ ...(where !== undefined ? { whereInSource: where } : {}),
150
+ nextAction: 'the anchor matched several elements, so which one the recording meant is not decidable. ' +
151
+ 'Narrow the anchor on this step — a unique testid, or a role+name that only one control ' +
152
+ 'has — rather than rebinding it; nothing here says the element was renamed or removed.',
153
+ };
154
+ }
126
155
  const fix = drift.nearest !== null && drift.ambiguous !== true
127
156
  ? `rebind the anchor to "${drift.nearest}" (closest survivor)`
128
157
  : drift.nearest !== null
@@ -207,6 +236,11 @@ isolation) {
207
236
  const unverifiable = [];
208
237
  let passed = 0;
209
238
  for (const { replay, flow } of runs) {
239
+ if (replay.status === ReplayStatus.UNVERIFIABLE) {
240
+ const reason = replay.unverifiable?.reason ?? unverifiableReason(flow) ?? 'the flow could not be graded';
241
+ unverifiable.push({ flow: replay.name, reason });
242
+ continue;
243
+ }
210
244
  if (replay.status === ReplayStatus.OK) {
211
245
  /*
212
246
  * The REPLAY's own answer first, then the grader's.
@@ -29,17 +29,63 @@ export declare function nearestIsAmbiguous(missing: string, present: string[]):
29
29
  /** Build the legible-drift record for a testid anchor that resolved to zero live elements. */
30
30
  export declare function testidDrift(value: string, hint: QueryEmptyHint | undefined): Drift;
31
31
  /**
32
- * Build the drift for a step whose anchor matched MORE THAN ONE live element.
32
+ * What the stale-ref retry found when it ran the locator again.
33
33
  *
34
- * `ambiguous: true` is set because that is exactly what it is, and it is the field heal already
35
- * reads to refuse an auto-rebind -- an ambiguous drift must never be healed to an arbitrary pick,
36
- * which is the same reasoning that made this a drift instead of a note in the first place.
34
+ * A discriminated result rather than the bare `string | undefined` this started as, because the two
35
+ * ways of getting no ref back are different answers and were arriving as one. `{ drift }` says the
36
+ * locator now names SEVERAL elements — the re-render turned an unambiguous anchor into an ambiguous
37
+ * one, and the fix is a NARROWER anchor. `undefined` says it named none, or named one and the second
38
+ * dispatch failed too; both of those are already told truthfully by the stale-ref error the first
39
+ * attempt produced, so there is nothing to add.
37
40
  *
38
- * `nearest` is null on purpose. There is no nearest: the anchor was found, several times over, and
39
- * proposing one of the matches as "the" match would re-make the guess this reason exists to stop.
40
- * The fix is a NARROWER anchor, and only a human or an agent looking at the page can choose it.
41
+ * Collapsed together, the ambiguous case was reported as "ref 'e-orig' no longer resolves to an
42
+ * element" — a sentence about an element that is GONE, sent for an element that is now three. An
43
+ * agent reading it hunts for a rename or re-records the flow; the truth is that the anchor needs
44
+ * narrowing, which is the whole reason `ANCHOR_AMBIGUOUS` exists as its own reason kind.
41
45
  */
42
- export declare function ambiguousAnchorDrift(value: string, matches: number): Drift;
46
+ export type Reresolved = {
47
+ readonly ref: string;
48
+ } | {
49
+ readonly drift: Drift;
50
+ };
51
+ /**
52
+ * The ambiguity rule itself: an anchor that matched several live elements is DRIFT, never a guess.
53
+ *
54
+ * Returns null when the locator named zero or one element, so a runner reads it as
55
+ * `const drift = ambiguityDrift(anchor, refs); if (drift !== null) return { …, ok: false, drift };`
56
+ * and never has to reconstruct the record itself.
57
+ *
58
+ * ONE function, because three anchor kinds ask the same question and each used to answer for itself.
59
+ * testid was fixed first and role second, and role's fix was written as a copy of testid's — so "is
60
+ * this ambiguous" had two answers and COMPONENT had none at all. `runComponentStep` took `refs[0]`,
61
+ * dispatched and returned `ok: true` whenever `by:'component'` matched several elements, which is the
62
+ * ordinary case for a component rendered once per row (a table row's action button). The verdict
63
+ * reads `drift` and `ok` and nothing else, so that green was indistinguishable from a replay that did
64
+ * what it did before — the #1227 defect, one anchor kind over.
65
+ *
66
+ * The caller must not dispatch before checking. Acting and then reporting drift would leave the app
67
+ * changed by a click nobody can attribute, which is worse than the ambiguity it reports.
68
+ */
69
+ export declare function ambiguityDrift(anchor: FlowAnchor, refs: readonly string[]): Drift | null;
70
+ /**
71
+ * The one ref a step means: the only match, or the recorded position among same-named controls
72
+ * while the page still has exactly as many of them as when it was recorded. Otherwise undefined.
73
+ */
74
+ export declare function refFor(anchor: object, refs: readonly string[]): string | undefined;
75
+ /**
76
+ * The anchor as the stale-ref RETRY found it: one ref to dispatch at, an ambiguity to report, or
77
+ * neither.
78
+ *
79
+ * The same rule as `ambiguityDrift`, one layer deeper, and shared for the same reason: a re-render
80
+ * can turn an unambiguous anchor into an ambiguous one between the two dispatches, and dispatching
81
+ * at the first of the new matches would re-make the guess the caller's own check exists to refuse —
82
+ * in a place the caller cannot see. Written once so the three runners cannot answer it differently,
83
+ * which is how the component runner came to have no answer at all.
84
+ *
85
+ * `refs[0]` is read only after `ambiguityDrift` has cleared the several-match case, so the only way
86
+ * it is undefined here is zero matches.
87
+ */
88
+ export declare function reresolved(anchor: FlowAnchor, refs: readonly string[]): Reresolved | undefined;
43
89
  /**
44
90
  * Build the drift for a step whose `expect.element` testid was absent after the action ran.
45
91
  *
@@ -139,6 +139,34 @@ export function testidDrift(value, hint) {
139
139
  drift.ambiguous = true;
140
140
  return drift;
141
141
  }
142
+ /**
143
+ * How an anchor is ADDRESSED — the sentence that names it, and the label a result row carries.
144
+ *
145
+ * Two different strings, and neither can be derived from the other: `value` is a raw testid for one
146
+ * anchor kind and a human-readable label for the others, so one sentence cannot name both. Calling a
147
+ * role anchor `testid "button "View result""` sends the reader to edit a locator that does not exist
148
+ * on that step, which is the same misdirection the drift reason exists to remove.
149
+ *
150
+ * Derived from the anchor rather than passed in by the caller. The first version of the role fix took
151
+ * a `described` argument and wrote `ambiguousAnchorDrift(label, n, \`role anchor ${label}\`)` — the
152
+ * same string twice, so the `anchor` field and the sentence could drift apart with nothing to catch
153
+ * it, and the next anchor kind would have had to remember the convention on its own.
154
+ */
155
+ function anchorAddress(anchor) {
156
+ if (anchor.kind === AnchorKind.TESTID) {
157
+ return { described: `testid "${anchor.value}"`, label: anchor.value };
158
+ }
159
+ if (anchor.kind === AnchorKind.ROLE) {
160
+ // Kind named explicitly: `button "View result"` alone reads as a testid that happens to contain
161
+ // a space, and the reader would go looking for a locator nothing on the step declares.
162
+ const label = `${anchor.role} "${String(anchor.name)}"`;
163
+ return { described: `role anchor ${label}`, label };
164
+ }
165
+ // A component needs no such qualifier: `componentLabel` already answers
166
+ // `NewDeployButton@Deployments.tsx:107`, which no reader mistakes for a testid.
167
+ const label = anchor.kind === AnchorKind.COMPONENT ? componentLabel(anchor) : anchorLabel(anchor);
168
+ return { described: `anchor ${label}`, label };
169
+ }
142
170
  /**
143
171
  * Build the drift for a step whose anchor matched MORE THAN ONE live element.
144
172
  *
@@ -149,16 +177,73 @@ export function testidDrift(value, hint) {
149
177
  * `nearest` is null on purpose. There is no nearest: the anchor was found, several times over, and
150
178
  * proposing one of the matches as "the" match would re-make the guess this reason exists to stop.
151
179
  * The fix is a NARROWER anchor, and only a human or an agent looking at the page can choose it.
180
+ *
181
+ * Private: callers reach it through `ambiguityDrift`, which is the RULE rather than the record.
182
+ * Exported, it had three call sites and a fourth anchor kind that never called it at all.
152
183
  */
153
- export function ambiguousAnchorDrift(value, matches) {
184
+ function ambiguousAnchorDrift(anchor, matches) {
185
+ const { described, label } = anchorAddress(anchor);
154
186
  return {
155
187
  reasonKind: DriftReason.ANCHOR_AMBIGUOUS,
156
- reason: `testid "${value}" matched ${String(matches)} live elements — which one the recording meant is not decidable; anchor this step to something unique`,
157
- anchor: value,
188
+ reason: `${described} matched ${String(matches)} live elements — which one the recording meant is not decidable; anchor this step to something unique`,
189
+ anchor: label,
158
190
  nearest: null,
159
191
  ambiguous: true,
160
192
  };
161
193
  }
194
+ /**
195
+ * The ambiguity rule itself: an anchor that matched several live elements is DRIFT, never a guess.
196
+ *
197
+ * Returns null when the locator named zero or one element, so a runner reads it as
198
+ * `const drift = ambiguityDrift(anchor, refs); if (drift !== null) return { …, ok: false, drift };`
199
+ * and never has to reconstruct the record itself.
200
+ *
201
+ * ONE function, because three anchor kinds ask the same question and each used to answer for itself.
202
+ * testid was fixed first and role second, and role's fix was written as a copy of testid's — so "is
203
+ * this ambiguous" had two answers and COMPONENT had none at all. `runComponentStep` took `refs[0]`,
204
+ * dispatched and returned `ok: true` whenever `by:'component'` matched several elements, which is the
205
+ * ordinary case for a component rendered once per row (a table row's action button). The verdict
206
+ * reads `drift` and `ok` and nothing else, so that green was indistinguishable from a replay that did
207
+ * what it did before — the #1227 defect, one anchor kind over.
208
+ *
209
+ * The caller must not dispatch before checking. Acting and then reporting drift would leave the app
210
+ * changed by a click nobody can attribute, which is worse than the ambiguity it reports.
211
+ */
212
+ export function ambiguityDrift(anchor, refs) {
213
+ return refs.length > 1 && refFor(anchor, refs) === undefined
214
+ ? ambiguousAnchorDrift(anchor, refs.length)
215
+ : null;
216
+ }
217
+ /**
218
+ * The one ref a step means: the only match, or the recorded position among same-named controls
219
+ * while the page still has exactly as many of them as when it was recorded. Otherwise undefined.
220
+ */
221
+ export function refFor(anchor, refs) {
222
+ if (1 === refs.length)
223
+ return refs[0];
224
+ const { nth, of } = { ...anchor };
225
+ return 'number' === typeof nth && of === refs.length ? refs[nth] : undefined;
226
+ }
227
+ /**
228
+ * The anchor as the stale-ref RETRY found it: one ref to dispatch at, an ambiguity to report, or
229
+ * neither.
230
+ *
231
+ * The same rule as `ambiguityDrift`, one layer deeper, and shared for the same reason: a re-render
232
+ * can turn an unambiguous anchor into an ambiguous one between the two dispatches, and dispatching
233
+ * at the first of the new matches would re-make the guess the caller's own check exists to refuse —
234
+ * in a place the caller cannot see. Written once so the three runners cannot answer it differently,
235
+ * which is how the component runner came to have no answer at all.
236
+ *
237
+ * `refs[0]` is read only after `ambiguityDrift` has cleared the several-match case, so the only way
238
+ * it is undefined here is zero matches.
239
+ */
240
+ export function reresolved(anchor, refs) {
241
+ const drift = ambiguityDrift(anchor, refs);
242
+ if (drift !== null)
243
+ return { drift };
244
+ const ref = refFor(anchor, refs);
245
+ return ref === undefined ? undefined : { ref };
246
+ }
162
247
  /**
163
248
  * Build the drift for a step whose `expect.element` testid was absent after the action ran.
164
249
  *
@@ -0,0 +1,15 @@
1
+ import type { Contradiction, FlowStepResult } from '@reticlehq/core';
2
+ /** Cross-step findings belong to no single step; -1 is the address the suite verdict already uses. */
3
+ export declare const CROSS_STEP_INDEX = -1;
4
+ /**
5
+ * The contradictions the whole-span pass found that no individual step could.
6
+ *
7
+ * A step's window closes when the step ends, so a request fired at step 2 and still unanswered at
8
+ * step 5 is invisible to every per-step window: step 2's closed before the answer came and step 5
9
+ * never saw it start. Re-running the detectors over the whole replay span finds those — and re-finds
10
+ * everything the steps already reported, which is what the subtraction is for. Reporting a finding
11
+ * twice teaches a reader that the count is noise.
12
+ *
13
+ * Exported for its own test: the subtraction is the whole rule, and it is pure.
14
+ */
15
+ export declare function crossStepOnly(whole: readonly Contradiction[], steps: readonly FlowStepResult[]): Contradiction[];