@reticlehq/server 3.5.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 (328) 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/cli-flow-commands.js +4 -8
  10. package/dist/command/cli/cli-parse.d.ts +1 -1
  11. package/dist/command/cli/cli-parse.js +10 -10
  12. package/dist/command/cli/cli-verify.d.ts +1 -1
  13. package/dist/command/cli/cli-verify.js +35 -25
  14. package/dist/command/cli/cloud-cli.js +51 -12
  15. package/dist/command/cli/cloud-login.d.ts +8 -0
  16. package/dist/command/cli/cloud-login.js +36 -2
  17. package/dist/command/cli/demo/demo-page.js +11 -3
  18. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +46 -0
  19. package/dist/command/cli/doctor/browser/chromium-hint.js +99 -1
  20. package/dist/command/cli/drive/drive-attach.d.ts +27 -2
  21. package/dist/command/cli/drive/drive-attach.js +71 -4
  22. package/dist/command/cli/drive/drive-command.d.ts +2 -1
  23. package/dist/command/cli/drive/drive-command.js +13 -7
  24. package/dist/command/cli/lifecycle/daemon-lifecycle.js +4 -1
  25. package/dist/command/cli/ports/port-holder.d.ts +10 -0
  26. package/dist/command/cli/ports/port-holder.js +24 -0
  27. package/dist/command/cli/setup-mcp-cli.js +6 -4
  28. package/dist/command/cli/tutorial.d.ts +2 -2
  29. package/dist/command/cli/tutorial.js +6 -9
  30. package/dist/command/cli.d.ts +6 -0
  31. package/dist/command/cli.js +28 -22
  32. package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
  33. package/dist/command/daemon/binding/daemon-status-probe.js +1 -0
  34. package/dist/command/daemon/daemon-resolve.d.ts +14 -0
  35. package/dist/command/daemon/daemon-resolve.js +33 -2
  36. package/dist/command/setup/agent-writer.d.ts +1 -1
  37. package/dist/command/setup/agent-writer.js +3 -2
  38. package/dist/command/setup/bringup/connect-lease.d.ts +1 -0
  39. package/dist/command/setup/bringup/connect-lease.js +1 -0
  40. package/dist/command/setup/bringup/dev-server-wait.d.ts +8 -0
  41. package/dist/command/setup/bringup/dev-server-wait.js +19 -0
  42. package/dist/command/setup/init/init-port.d.ts +4 -0
  43. package/dist/command/setup/init/init-port.js +10 -0
  44. package/dist/command/setup/init/init-runtime.d.ts +10 -0
  45. package/dist/command/setup/init/init-runtime.js +18 -1
  46. package/dist/command/setup/node-effects.js +2 -1
  47. package/dist/command/setup/remaining-steps.js +2 -1
  48. package/dist/command/setup/run-setup.d.ts +5 -0
  49. package/dist/command/setup/run-setup.js +35 -27
  50. package/dist/command/setup/setup-command.d.ts +2 -1
  51. package/dist/command/setup/setup-command.js +2 -0
  52. package/dist/command/version/version-skew.js +11 -2
  53. package/dist/features/crawl/crawl-tools.js +4 -0
  54. package/dist/features/crawl/crawl.d.ts +6 -0
  55. package/dist/features/crawl/crawl.js +9 -1
  56. package/dist/features/exhaust/ledger.d.ts +7 -0
  57. package/dist/features/exhaust/ledger.js +35 -9
  58. package/dist/features/harness/app-findings.d.ts +2 -0
  59. package/dist/features/harness/app-findings.js +52 -0
  60. package/dist/features/harness/drive-report.d.ts +27 -1
  61. package/dist/features/harness/drive-report.js +162 -11
  62. package/dist/features/harness/drivers.d.ts +10 -14
  63. package/dist/features/harness/drivers.js +10 -14
  64. package/dist/features/harness/fill-values.d.ts +3 -73
  65. package/dist/features/harness/fill-values.js +3 -169
  66. package/dist/features/harness/goals.d.ts +21 -0
  67. package/dist/features/harness/goals.js +77 -0
  68. package/dist/features/harness/harness.d.ts +21 -0
  69. package/dist/features/harness/harness.js +69 -2
  70. package/dist/features/harness/platform/drive-target.d.ts +30 -0
  71. package/dist/features/harness/platform/drive-target.js +81 -0
  72. package/dist/features/harness/platform/local-apps.d.ts +94 -0
  73. package/dist/features/harness/platform/local-apps.js +204 -0
  74. package/dist/features/harness/platform/remote-drive.d.ts +180 -0
  75. package/dist/features/harness/platform/remote-drive.js +365 -0
  76. package/dist/features/harness/platform/script.d.ts +48 -0
  77. package/dist/features/harness/platform/script.js +128 -0
  78. package/dist/features/harness/platform/server-driver.d.ts +41 -0
  79. package/dist/features/harness/platform/server-driver.js +179 -0
  80. package/dist/features/harness/platform-config.d.ts +7 -0
  81. package/dist/features/harness/platform-config.js +14 -2
  82. package/dist/features/harness/script/app-controls.d.ts +5 -0
  83. package/dist/features/harness/script/app-controls.js +40 -0
  84. package/dist/features/harness/script/lane-ids.d.ts +14 -0
  85. package/dist/features/harness/script/lane-ids.js +16 -0
  86. package/dist/features/harness/script/script-run.d.ts +41 -0
  87. package/dist/features/harness/script/script-run.js +299 -0
  88. package/dist/features/license/license.d.ts +2 -2
  89. package/dist/features/visual/visual-tools.d.ts +10 -1
  90. package/dist/features/visual/visual-tools.js +40 -3
  91. package/dist/hooks/driven-by.d.ts +4 -0
  92. package/dist/hooks/driven-by.js +16 -0
  93. package/dist/hooks/hook-commands.js +15 -4
  94. package/dist/hooks/tool-log.d.ts +9 -0
  95. package/dist/hooks/tool-log.js +29 -0
  96. package/dist/index.d.ts +1 -0
  97. package/dist/index.js +76 -82
  98. package/dist/judgement/runs/artifact/build-verification-run.js +6 -0
  99. package/dist/judgement/runs/artifact/replay-mapping.d.ts +1 -1
  100. package/dist/judgement/runs/artifact/replay-mapping.js +16 -6
  101. package/dist/judgement/runs/artifact/run-store.d.ts +1 -1
  102. package/dist/judgement/runs/artifact/run-store.js +26 -1
  103. package/dist/judgement/runs/drive-run.d.ts +22 -0
  104. package/dist/judgement/runs/drive-run.js +99 -1
  105. package/dist/judgement/runs/repair-prompt.js +1 -1
  106. package/dist/judgement/runs/reticle-runner.js +8 -1
  107. package/dist/judgement/runs/verification-sync.d.ts +3 -2
  108. package/dist/judgement/runs/verification-sync.js +4 -3
  109. package/dist/language/flows/change/gate.d.ts +10 -0
  110. package/dist/language/flows/change/gate.js +22 -0
  111. package/dist/language/flows/decision.js +34 -0
  112. package/dist/language/flows/flow-anchor.d.ts +54 -8
  113. package/dist/language/flows/flow-anchor.js +88 -3
  114. package/dist/language/flows/flow-cross-step.d.ts +15 -0
  115. package/dist/language/flows/flow-cross-step.js +21 -0
  116. package/dist/language/flows/flow-learning.js +8 -11
  117. package/dist/language/flows/flow-mutate-tools.js +5 -3
  118. package/dist/language/flows/flow-replay-run.d.ts +23 -9
  119. package/dist/language/flows/flow-replay-run.js +101 -94
  120. package/dist/language/flows/flow-replay.d.ts +23 -1
  121. package/dist/language/flows/flow-replay.js +92 -19
  122. package/dist/language/flows/flow-scope.js +5 -1
  123. package/dist/language/flows/flow-step-runners.d.ts +20 -2
  124. package/dist/language/flows/flow-step-runners.js +236 -42
  125. package/dist/language/flows/flow-store-for-session.d.ts +2 -2
  126. package/dist/language/flows/flow-store-for-session.js +2 -2
  127. package/dist/language/flows/flow-tools.js +58 -34
  128. package/dist/language/flows/flows.js +5 -2
  129. package/dist/language/flows/heal-run.js +6 -4
  130. package/dist/language/flows/recording/flake.d.ts +2 -2
  131. package/dist/language/flows/recording/tape/recordings.d.ts +30 -4
  132. package/dist/language/flows/recording/tape/recordings.js +40 -8
  133. package/dist/language/flows/replay-from-hud.d.ts +20 -0
  134. package/dist/language/flows/replay-from-hud.js +51 -0
  135. package/dist/language/flows/replay.d.ts +9 -5
  136. package/dist/language/flows/replay.js +27 -9
  137. package/dist/language/flows/suite/suite-flakes.js +2 -0
  138. package/dist/language/flows/suite-selection.d.ts +2 -15
  139. package/dist/language/flows/suite-selection.js +4 -3
  140. package/dist/language/flows/verify-change-tools.js +8 -4
  141. package/dist/launch-chromium.d.ts +27 -5
  142. package/dist/launch-chromium.js +38 -8
  143. package/dist/memory/cloud/cloud-config.d.ts +12 -0
  144. package/dist/memory/cloud/cloud-config.js +15 -0
  145. package/dist/memory/cloud/cloud-sync.d.ts +17 -14
  146. package/dist/memory/cloud/cloud-sync.js +12 -1
  147. package/dist/memory/cloud/harness-config.d.ts +26 -1
  148. package/dist/memory/cloud/harness-config.js +99 -4
  149. package/dist/memory/cloud/hud-notices-source.d.ts +21 -0
  150. package/dist/memory/cloud/hud-notices-source.js +128 -0
  151. package/dist/memory/cloud/memory-scope.d.ts +144 -0
  152. package/dist/memory/cloud/memory-scope.js +205 -0
  153. package/dist/memory/cloud/sync-cycle.d.ts +12 -3
  154. package/dist/memory/cloud/sync-cycle.js +16 -3
  155. package/dist/memory/cloud/sync-daemon.js +7 -5
  156. package/dist/memory/cloud/sync-disk.d.ts +1 -1
  157. package/dist/memory/cloud/sync-disk.js +5 -8
  158. package/dist/memory/impact/account-watch.d.ts +19 -0
  159. package/dist/memory/impact/account-watch.js +34 -0
  160. package/dist/memory/impact/impact-recorder.d.ts +8 -0
  161. package/dist/memory/impact/impact-recorder.js +12 -2
  162. package/dist/memory/impact/impact-store.d.ts +16 -0
  163. package/dist/memory/impact/impact-store.js +142 -26
  164. package/dist/memory/intent/intent-shard.d.ts +2 -2
  165. package/dist/memory/intent/intent-tools.js +80 -2
  166. package/dist/memory/journal/envelope.d.ts +8 -8
  167. package/dist/memory/journal/journal-recorder.js +7 -1
  168. package/dist/memory/journal/on-disk/retention.d.ts +3 -0
  169. package/dist/memory/journal/on-disk/retention.js +5 -0
  170. package/dist/memory/journal/on-disk/startup-maintenance.js +3 -1
  171. package/dist/memory/journal/on-disk/workspace-tiers.js +7 -4
  172. package/dist/memory/journal/session-end.js +6 -4
  173. package/dist/memory/project/artifact-root-resolver.d.ts +26 -1
  174. package/dist/memory/project/artifact-root-resolver.js +94 -13
  175. package/dist/memory/project/artifact-root.d.ts +42 -5
  176. package/dist/memory/project/artifact-root.js +87 -8
  177. package/dist/memory/project/dir/reticle-dir.d.ts +4 -2
  178. package/dist/memory/project/dir/reticle-dir.js +3 -1
  179. package/dist/memory/project/project-tools.js +22 -3
  180. package/dist/memory/project/session-root.d.ts +24 -1
  181. package/dist/memory/project/session-root.js +41 -5
  182. package/dist/memory/project/sync-status.d.ts +39 -0
  183. package/dist/memory/project/sync-status.js +90 -0
  184. package/dist/memory/recall/memory-tools.js +2 -0
  185. package/dist/memory/recall/project-memory.d.ts +19 -0
  186. package/dist/memory/recall/project-memory.js +45 -6
  187. package/dist/portal/bridge/bridge.d.ts +3 -1
  188. package/dist/portal/bridge/bridge.js +19 -2
  189. package/dist/portal/bridge/pairing-token.d.ts +9 -0
  190. package/dist/portal/bridge/pairing-token.js +17 -0
  191. package/dist/portal/input/real-input.d.ts +81 -10
  192. package/dist/portal/input/real-input.js +207 -22
  193. package/dist/portal/input/viewport-tools.js +3 -3
  194. package/dist/portal/pool/browser-pool.d.ts +22 -9
  195. package/dist/portal/pool/browser-pool.js +70 -35
  196. package/dist/portal/pool/playwright-launcher.d.ts +13 -0
  197. package/dist/portal/pool/playwright-launcher.js +17 -1
  198. package/dist/portal/pool/pool-contract.d.ts +7 -4
  199. package/dist/portal/session/command-payload.d.ts +8 -0
  200. package/dist/portal/session/command-payload.js +19 -0
  201. package/dist/portal/session/dev-server/dev-command.d.ts +12 -0
  202. package/dist/portal/session/dev-server/dev-command.js +61 -12
  203. package/dist/portal/session/facts/departure-note.d.ts +22 -0
  204. package/dist/portal/session/facts/departure-note.js +47 -0
  205. package/dist/portal/session/facts/session-replaced.d.ts +5 -0
  206. package/dist/portal/session/facts/session-replaced.js +5 -0
  207. package/dist/portal/session/first-run-wiring.d.ts +42 -0
  208. package/dist/portal/session/first-run-wiring.js +132 -0
  209. package/dist/portal/session/human/notes-ledger.d.ts +20 -0
  210. package/dist/portal/session/human/notes-ledger.js +55 -0
  211. package/dist/portal/session/human/review-store.d.ts +12 -0
  212. package/dist/portal/session/human/review-store.js +18 -10
  213. package/dist/portal/session/live-control-tools.d.ts +0 -13
  214. package/dist/portal/session/live-control-tools.js +28 -3
  215. package/dist/portal/session/no-session-diagnosis.d.ts +7 -0
  216. package/dist/portal/session/no-session-diagnosis.js +15 -0
  217. package/dist/portal/session/no-session-next-action.d.ts +11 -0
  218. package/dist/portal/session/no-session-next-action.js +31 -5
  219. package/dist/portal/session/no-session-watch.d.ts +14 -1
  220. package/dist/portal/session/no-session-watch.js +63 -10
  221. package/dist/portal/session/presence/session-mirror.js +3 -0
  222. package/dist/portal/session/serving-directory.d.ts +27 -0
  223. package/dist/portal/session/serving-directory.js +45 -0
  224. package/dist/portal/session/session-health.d.ts +7 -0
  225. package/dist/portal/session/session-health.js +6 -0
  226. package/dist/portal/session/session-info.d.ts +8 -1
  227. package/dist/portal/session/session-info.js +6 -0
  228. package/dist/portal/session/session-manager.d.ts +21 -0
  229. package/dist/portal/session/session-manager.js +44 -8
  230. package/dist/portal/session/session-reaper.js +4 -1
  231. package/dist/portal/session/session-successor.d.ts +5 -0
  232. package/dist/portal/session/session-tools.js +26 -3
  233. package/dist/portal/session/session.d.ts +7 -2
  234. package/dist/portal/session/session.js +21 -18
  235. package/dist/start-options.d.ts +6 -0
  236. package/dist/status-payload.d.ts +10 -0
  237. package/dist/status-payload.js +23 -0
  238. package/dist/surface/http-server.js +11 -1
  239. package/dist/surface/mcp/mcp-proxy.d.ts +5 -0
  240. package/dist/surface/mcp/mcp-proxy.js +36 -5
  241. package/dist/surface/mcp/mcp.d.ts +11 -6
  242. package/dist/surface/mcp/mcp.js +27 -13
  243. package/dist/surface/mcp/server-instructions.d.ts +4 -2
  244. package/dist/surface/mcp/server-instructions.js +14 -4
  245. package/dist/surface/mcp/shared-params.js +2 -2
  246. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +8 -0
  247. package/dist/surface/tools/act/navigation/navigate-arrival.js +56 -2
  248. package/dist/surface/tools/act/settle-in-flight.js +25 -3
  249. package/dist/surface/tools/act-sequence-tool.js +2 -2
  250. package/dist/surface/tools/act-tools.js +23 -7
  251. package/dist/surface/tools/assert/assert-verdict.d.ts +1 -1
  252. package/dist/surface/tools/assert/assert-verdict.js +17 -2
  253. package/dist/surface/tools/browser-tools.js +8 -1
  254. package/dist/surface/tools/chat-drives.d.ts +26 -0
  255. package/dist/surface/tools/chat-drives.js +165 -0
  256. package/dist/surface/tools/empty-session-list.d.ts +41 -0
  257. package/dist/surface/tools/empty-session-list.js +80 -0
  258. package/dist/surface/tools/error-recovery.d.ts +6 -0
  259. package/dist/surface/tools/error-recovery.js +37 -4
  260. package/dist/surface/tools/explore-tools.d.ts +10 -1
  261. package/dist/surface/tools/explore-tools.js +82 -13
  262. package/dist/surface/tools/feedback-tools.js +14 -0
  263. package/dist/surface/tools/harness-explore.d.ts +96 -14
  264. package/dist/surface/tools/harness-explore.js +227 -206
  265. package/dist/surface/tools/harness-plan.d.ts +12 -0
  266. package/dist/surface/tools/harness-plan.js +31 -0
  267. package/dist/surface/tools/harness-script.d.ts +8 -0
  268. package/dist/surface/tools/harness-script.js +342 -0
  269. package/dist/surface/tools/harness-toolset.d.ts +14 -1
  270. package/dist/surface/tools/harness-toolset.js +77 -6
  271. package/dist/surface/tools/invoke-tool.js +52 -1
  272. package/dist/surface/tools/lease-seed.d.ts +29 -0
  273. package/dist/surface/tools/lease-seed.js +123 -0
  274. package/dist/surface/tools/lease-tools.d.ts +3 -23
  275. package/dist/surface/tools/lease-tools.js +30 -131
  276. package/dist/surface/tools/live-call-text.js +2 -2
  277. package/dist/surface/tools/next-step.d.ts +19 -0
  278. package/dist/surface/tools/next-step.js +159 -0
  279. package/dist/surface/tools/observe-tools.js +2 -4
  280. package/dist/surface/tools/platform-moment.d.ts +12 -0
  281. package/dist/surface/tools/platform-moment.js +168 -0
  282. package/dist/surface/tools/query-hint-schema.d.ts +7 -0
  283. package/dist/surface/tools/query-hint-schema.js +10 -0
  284. package/dist/surface/tools/read-tools.js +8 -2
  285. package/dist/surface/tools/real-input-attempt.js +96 -13
  286. package/dist/surface/tools/tool-kit.d.ts +15 -1
  287. package/dist/surface/tools/tool-kit.js +4 -6
  288. package/dist/surface/tools/tools.js +20 -48
  289. package/dist/telemetry/hud-metrics.d.ts +2 -0
  290. package/dist/telemetry/hud-metrics.js +17 -0
  291. package/dist/telemetry/telemetry.d.ts +12 -0
  292. package/dist/telemetry/telemetry.js +43 -14
  293. package/dist/telemetry/verification-of.js +1 -0
  294. package/dist/wire-journal.js +4 -1
  295. package/docs/README.md +1 -1
  296. package/docs/agent-cheatsheet.md +1 -1
  297. package/docs/autodrive.md +29 -25
  298. package/docs/cli/doctor.mdx +6 -0
  299. package/docs/cli/drive.mdx +8 -2
  300. package/docs/cli/init.mdx +11 -13
  301. package/docs/cli/status.mdx +5 -1
  302. package/docs/cli/version.mdx +3 -3
  303. package/docs/cli.mdx +1 -1
  304. package/docs/desktop.mdx +5 -1
  305. package/docs/environment.mdx +3 -7
  306. package/docs/getting-started.md +13 -11
  307. package/docs/human-control.md +1 -2
  308. package/docs/index.mdx +4 -3
  309. package/docs/install-manual.mdx +2 -2
  310. package/docs/packages/tauri.mdx +2 -2
  311. package/docs/packages/vite-plugin.mdx +22 -1
  312. package/docs/packages.mdx +1 -1
  313. package/docs/quickstart.mdx +17 -7
  314. package/docs/telemetry.md +1 -1
  315. package/docs/tools/session-and-feedback.mdx +9 -9
  316. package/docs/usage.md +5 -2
  317. package/docs/what-is-recorded.md +13 -3
  318. package/docs/what-is-reticle.mdx +3 -1
  319. package/package.json +8 -12
  320. package/dist/features/harness/driver.d.ts +0 -50
  321. package/dist/features/harness/driver.js +0 -236
  322. package/dist/features/harness/jev-driver.d.ts +0 -188
  323. package/dist/features/harness/jev-driver.js +0 -678
  324. package/dist/features/harness/openai-driver.d.ts +0 -58
  325. package/dist/features/harness/openai-driver.js +0 -206
  326. package/dist/memory/project/dir/fill-value-store.d.ts +0 -32
  327. package/dist/memory/project/dir/fill-value-store.js +0 -88
  328. package/docs/local-registry.md +0 -143
@@ -6,7 +6,7 @@ import { emptyFlowRefusal } from './empty-flow.js';
6
6
  import { aliasParam } from '../../surface/tools/args/alias-args.js';
7
7
  import { FlowErrorCode, RecordedSaveError, ReplayStatus, SuiteIsolation, } from '@reticlehq/core';
8
8
  import { recordSuiteFlakes } from './suite/suite-flakes.js';
9
- import { ReticleTool } from '@reticlehq/core';
9
+ import { AppRuntime, ReticleTool } from '@reticlehq/core';
10
10
  import { asNumber, asString, mutationTargetsFor, perturbationFor } from '@reticlehq/core';
11
11
  import { workerCountSchema } from '../../surface/tools/args/numeric-bounds.js';
12
12
  import { log } from '../../log.js';
@@ -21,7 +21,8 @@ import { recordingBacktrackWarning } from './recording/recording-backtrack.js';
21
21
  import { isValidFlowName, flowPath } from '../../memory/project/dir/reticle-dir.js';
22
22
  import {} from './flows.js';
23
23
  import { flowsForSession } from './flow-store-for-session.js';
24
- import { rootForProjectId } from '../../memory/project/session-root.js';
24
+ import { rootForTarget, sessionTarget } from '../../memory/project/session-root.js';
25
+ import { originOf } from '../../portal/session/session-manager.js';
25
26
  import { resolveSuiteSelection } from './suite-selection.js';
26
27
  import { replayNamedFlow, flowErrorMessage, latestRecordedFlow, sessionProjectId, } from './flow-replay-run.js';
27
28
  import { persistAndSyncVerificationRun, } from '../../judgement/runs/verification-sync.js';
@@ -34,19 +35,21 @@ import { replayAndLearn } from './flow-learning.js';
34
35
  * set). Fire-and-forget: resolves the config, POSTs via the platform fetch, and logs the outcome. Any
35
36
  * failure is swallowed so a network hiccup never affects the local save.
36
37
  */
37
- async function syncSavedFlowToCloud(deps, flow, projectId) {
38
+ async function syncSavedFlowToCloud(deps, flow, target) {
38
39
  // Per-project cloud: sync a saved flow only when cloud is attached AND flow sync is enabled.
39
40
  // The link file of the project this flow belongs to — the flow itself was already saved through
40
41
  // `flowsForSession`, and reading the credential from a different tree is how one project's work
41
42
  // reaches another project's dashboard.
42
- const cloud = await resolveProjectCloud(deps.fs, rootForProjectId(deps, projectId), homedir(), process.env);
43
+ const cloud = await resolveProjectCloud(deps.fs, rootForTarget(deps, target), homedir(), process.env);
43
44
  if (null === cloud.config || !cloud.policy.flows)
44
45
  return; // not attached / flows disabled → local only
45
- const result = await syncFlowToCloud(flow, cloud.config, projectId, cloudFetch);
46
+ const result = await syncFlowToCloud(flow, cloud.config, target.projectId, cloudFetch);
46
47
  if (result.outcome !== SyncOutcome.SYNCED) {
47
48
  log('cloud-flow-sync-failed', { flow: flow.name, status: result.status, error: result.error });
48
49
  }
49
50
  }
51
+ /** A step index no recorded flow reaches; bounds `to` and `at`. */
52
+ const MAX_REPLAY_STEP = 10_000;
50
53
  /**
51
54
  * The URL a leased context should open: the app's ORIGIN, not the live tab's current location.
52
55
  *
@@ -57,7 +60,10 @@ async function syncSavedFlowToCloud(deps, flow, projectId) {
57
60
  */
58
61
  export function leasableAppUrl(deps, sessionId) {
59
62
  try {
60
- const url = deps.sessions.resolve(sessionId).url;
63
+ const session = deps.sessions.resolve(sessionId);
64
+ if (AppRuntime.WEB !== (session.runtime ?? AppRuntime.WEB))
65
+ return undefined; // desktop: no IPC in a lease
66
+ const url = session.url;
61
67
  if (typeof url !== 'string' || 0 === url.length)
62
68
  return undefined;
63
69
  return new URL(url).origin;
@@ -211,13 +217,14 @@ export const FLOW_TOOLS = [
211
217
  ...(success !== undefined ? { success } : {}),
212
218
  ...(intent !== undefined ? { intent } : {}),
213
219
  };
214
- const projectId = sessionProjectId(deps, asString(args['sessionId']));
220
+ const target = sessionTarget(deps, asString(args['sessionId']));
221
+ const { projectId } = target;
215
222
  // A zero-step recording is not a flow. Saving it wrote a file the suite reports "unverifiable"
216
223
  // forever while telling the agent it had saved a regression test — see empty-flow.
217
224
  const emptyRefusal = emptyFlowRefusal(program.steps.length, name);
218
225
  if (emptyRefusal !== undefined)
219
226
  return Promise.resolve(emptyRefusal);
220
- const { flows, root } = flowsForSession(deps, projectId);
227
+ const { flows, root } = flowsForSession(deps, target);
221
228
  const author = flowAuthor();
222
229
  const toSave = {
223
230
  ...program,
@@ -275,10 +282,11 @@ export const FLOW_TOOLS = [
275
282
  // (the prior bug) made schema-validating MCP clients reject the result ("expected object,
276
283
  // received string") — caught driving the live demo.
277
284
  handler: (deps, args) => {
278
- const projectId = sessionProjectId(deps, asString(args['sessionId']));
285
+ const target = sessionTarget(deps, asString(args['sessionId']));
286
+ const { projectId } = target;
279
287
  // The APP's flows, not the daemon's. Listing where the daemon was launched is what showed a
280
288
  // React dashboard a HUD full of Electron and Tauri flows from an unrelated checkout.
281
- const { flows: store, root } = flowsForSession(deps, projectId);
289
+ const { flows: store, root } = flowsForSession(deps, target);
282
290
  return store.list(projectId).then((names) => ({
283
291
  // `list` deliberately returns invalid names so they are reported rather than silently
284
292
  // dropped — but the declared outputSchema requires `path` on EVERY entry, and omitting it made
@@ -366,16 +374,15 @@ export const FLOW_TOOLS = [
366
374
  {
367
375
  name: ReticleTool.FLOW_REPLAY,
368
376
  description: "Replay a git-checked flow from .reticle/flows/<name>.json. RE-RESOLVES each step's semantic " +
369
- 'anchor (testid via reticle_query; signal via predicate) against the LIVE DOM — never reuses a ' +
370
- 'stale ref. On an anchor MISS returns legible DRIFT { step, anchor, drift:{ reasonKind, reason, ' +
371
- 'nearest } } (the closest surviving testid) and stops — the "whose fault is it" contract. ' +
372
- 'Returns { name, status: ok|drift|error, steps:[...] }; missing/malformed files and action ' +
373
- 'failures are status:error with a structured code (distinct from contract-changed drift). ' +
374
- 'An ok replay of a flow that asserts no consequence also carries unverifiable:{reason} — it ' +
375
- 'would read ok even if the feature were broken, so do NOT treat that ok as proof. ' +
376
- 'A run that stops early carries halted:{atStep, notAttempted}: `steps` is SHORTER than the ' +
377
- 'flow because replay stopped, not because a step was skipped — do not read a missing step as ' +
378
- 'an action that failed to dispatch.',
377
+ 'anchor (testid via reticle_query; signal via predicate) against LIVE DOM. On anchor MISS returns ' +
378
+ 'DRIFT { step, anchor, drift:{ reasonKind, reason, nearest } } and stops. ' +
379
+ 'Returns { name, status: ok|drift|error|unverifiable, steps:[...] }. ' +
380
+ 'Action failures are status:error. ' +
381
+ 'Status unverifiable means flow could not be graded (missing secrets, unmet preconditions, lost ' +
382
+ 'document) — neither pass nor fail, treat as UNKNOWN. ' +
383
+ 'An ok replay of a flow asserting no consequence also carries unverifiable:{reason} — do not ' +
384
+ 'treat as proof. ' +
385
+ 'Early halt carries halted:{atStep, notAttempted}: `steps` is shorter because replay stopped.',
379
386
  inputSchema: {
380
387
  flow: z
381
388
  .string()
@@ -392,6 +399,20 @@ export const FLOW_TOOLS = [
392
399
  .union([z.number().int().nonnegative(), z.string().min(1)])
393
400
  .optional()
394
401
  .describe('Resume at this step (index or step id). Earlier steps re-run as unchecked, unreported setup; a setup step marked effect:"commits" refuses the resume.'),
402
+ to: z
403
+ .number()
404
+ .int()
405
+ .positive()
406
+ .max(MAX_REPLAY_STEP)
407
+ .optional()
408
+ .describe('Stop before this step (drive only to a point journeys branch from).'),
409
+ at: z
410
+ .number()
411
+ .int()
412
+ .positive()
413
+ .max(MAX_REPLAY_STEP)
414
+ .optional()
415
+ .describe('Continue at this step on the page as it is (after another flow ran `to` it).'),
395
416
  sweep: z
396
417
  .boolean()
397
418
  .optional()
@@ -412,7 +433,7 @@ export const FLOW_TOOLS = [
412
433
  // The flow's name — always present in FlowReplayResult (the description promises `{ name, … }`),
413
434
  // but omitted here, so a validating profile stripped it and a replay result arrived anonymous.
414
435
  name: z.string(),
415
- status: z.string().describe('ok | drift | error'),
436
+ status: z.string().describe('ok | drift | error | unverifiable'),
416
437
  steps: z.array(z.unknown()),
417
438
  proposals: z.array(z.unknown()).optional(),
418
439
  deviation: z
@@ -482,8 +503,9 @@ export const FLOW_TOOLS = [
482
503
  * inherit it as a mystery — the same rule the mutation loop had to earn.
483
504
  */
484
505
  const sessionId = asString(args['sessionId']);
485
- const loaded = await flowsForSession(deps, sessionProjectId(deps, sessionId))
486
- .flows.load(asString(args['flowName']) ?? asString(args['flow']) ?? '', sessionProjectId(deps, sessionId))
506
+ const target = sessionTarget(deps, sessionId);
507
+ const loaded = await flowsForSession(deps, target)
508
+ .flows.load(asString(args['flowName']) ?? asString(args['flow']) ?? '', target.projectId)
487
509
  .catch(() => null);
488
510
  const targets = loaded !== null && loaded.ok ? mutationTargetsFor(loaded.value) : [];
489
511
  const rules = perturbationFor(seed, targets);
@@ -576,8 +598,9 @@ export const FLOW_TOOLS = [
576
598
  handler: async (deps, args) => {
577
599
  const sessionId = asString(args['sessionId']);
578
600
  // "Replay all" means all of THIS app's flows (+ legacy), not every project's on a shared daemon.
579
- const projectId = sessionProjectId(deps, sessionId);
580
- const selected = await resolveSuiteSelection(deps, projectId, args);
601
+ const target = sessionTarget(deps, sessionId);
602
+ const { projectId } = target;
603
+ const selected = await resolveSuiteSelection(deps, target, args);
581
604
  const requested = selected.run;
582
605
  /** Named rather than subtracted — a suite that returns fewer than were asked for must say so. */
583
606
  const heldBack = {
@@ -587,7 +610,7 @@ export const FLOW_TOOLS = [
587
610
  ...(selected.unsatisfied === undefined ? {} : { unsatisfied: selected.unsatisfied }),
588
611
  };
589
612
  // verify:server — hand the whole suite to the hosted runner; it records the verification itself.
590
- const cloud = await resolveProjectCloud(deps.fs, rootForProjectId(deps, projectId), homedir(), process.env);
613
+ const cloud = await resolveProjectCloud(deps.fs, rootForTarget(deps, target), homedir(), process.env);
591
614
  const server = await runServerVerify(deps, cloud, sessionId, requested);
592
615
  if (server !== null)
593
616
  return server;
@@ -639,7 +662,7 @@ export const FLOW_TOOLS = [
639
662
  const parallelRuns = await Promise.all(outcomes.map(async (o, i) => {
640
663
  const name = requested[i] ?? '';
641
664
  const replay = o.ok && o.value !== undefined ? o.value.replay : leaseFailureReplay(name, o.error);
642
- const loaded = await flowsForSession(deps, projectId)
665
+ const loaded = await flowsForSession(deps, target)
643
666
  .flows.load(name, projectId)
644
667
  .catch(() => null);
645
668
  const flow = loaded !== null && loaded.ok ? loaded.value : undefined;
@@ -649,8 +672,8 @@ export const FLOW_TOOLS = [
649
672
  ...(parallelRuns[i] ?? { replay: leaseFailureReplay(requested[i] ?? '', o.error) }),
650
673
  durationMs: o.ok && o.value !== undefined ? o.value.durationMs : 0,
651
674
  }));
652
- const flaky = await recordSuiteFlakes(deps.fs, rootForProjectId(deps, projectId), parallelRuns);
653
- await persistAndSyncVerificationRun(deps, timed, projectId);
675
+ const flaky = await recordSuiteFlakes(deps.fs, rootForTarget(deps, target), parallelRuns);
676
+ await persistAndSyncVerificationRun(deps, timed, target);
654
677
  const verdict = buildSuiteVerdict(parallelRuns, selected.knownRoutes, SuiteIsolation.PER_FLOW_LEASE);
655
678
  return {
656
679
  ...verdict,
@@ -674,7 +697,7 @@ export const FLOW_TOOLS = [
674
697
  for (const flowName of requested) {
675
698
  const start = deps.now();
676
699
  const replay = await replayAndLearn(deps, { flowName, sessionId });
677
- const loaded = await flowsForSession(deps, projectId)
700
+ const loaded = await flowsForSession(deps, target)
678
701
  .flows.load(flowName, projectId)
679
702
  .catch(() => null);
680
703
  const flow = loaded !== null && loaded.ok ? loaded.value : undefined;
@@ -689,9 +712,9 @@ export const FLOW_TOOLS = [
689
712
  // `reticle flow` on the command line, so an AGENT running this tool a hundred times learned
690
713
  // nothing about which flows are intermittent. Same product, same ledger, two surfaces, and the
691
714
  // one an agent uses was the blind half.
692
- const flaky = await recordSuiteFlakes(deps.fs, rootForProjectId(deps, projectId), runs);
715
+ const flaky = await recordSuiteFlakes(deps.fs, rootForTarget(deps, target), runs);
693
716
  // Emit the consolidated run artifact (Runs tab) + best-effort cloud push. Never blocks the verdict.
694
- await persistAndSyncVerificationRun(deps, timed, projectId);
717
+ await persistAndSyncVerificationRun(deps, timed, target);
695
718
  const verdict = buildSuiteVerdict(runs, selected.knownRoutes, SuiteIsolation.SHARED_SESSION);
696
719
  // A flow that has both passed and failed on UNCHANGED code is a different thing from a
697
720
  // regression, and an agent that cannot tell them apart either chases a ghost or ignores a real
@@ -766,12 +789,13 @@ export const FLOW_TOOLS = [
766
789
  // Resolved like every other path now. This save used to go to the daemon's root while the
767
790
  // recorded-flow save resolved per session, so where a flow landed depended on which tool wrote
768
791
  // it — and the pair that disagreed were the two ways to save the same thing.
769
- const res = await flowsForSession(deps, session.projectId).flows.saveFlow(flow, session.projectId);
792
+ const target = { projectId: session.projectId, origin: originOf(session.url) };
793
+ const res = await flowsForSession(deps, target).flows.saveFlow(flow, session.projectId);
770
794
  if (!res.ok)
771
795
  return { error: flowErrorMessage(res.code, res.detail), code: res.code };
772
796
  // If logged in to Reticle, mirror the saved flow to the team's regression suite. Best-effort
773
797
  // and non-blocking: the flow is already on disk, so a sync failure never fails the save.
774
- void syncSavedFlowToCloud(deps, flow, session.projectId);
798
+ void syncSavedFlowToCloud(deps, flow, target);
775
799
  // Return the SaveSummary as-is ({ name, stepCount, degraded, empty }) — the outputSchema
776
800
  // declares `name`, so the old `flowName` key was silently stripped by schema-strict clients.
777
801
  return res.value;
@@ -76,8 +76,11 @@ export function anchorForStep(args) {
76
76
  if (by === QueryBy.ROLE && value !== undefined) {
77
77
  const name = asString(args['name']);
78
78
  if (name !== undefined && name.length > 0) {
79
- // The schema carries role + name only; provenance lives on the step, not the anchor.
80
- return { anchor: { kind: AnchorKind.ROLE, role: value, name }, degraded: false };
79
+ // Provenance lives on the step, not the anchor; the position among namesakes is identity.
80
+ const nth = args['nth'];
81
+ const of = args['of'];
82
+ const among = 'number' === typeof nth && 'number' === typeof of ? { nth, of } : {};
83
+ return { anchor: { kind: AnchorKind.ROLE, role: value, name, ...among }, degraded: false };
81
84
  }
82
85
  }
83
86
  if (by === QueryBy.COMPONENT) {
@@ -12,7 +12,8 @@ import { replayFlow } from './flow-replay.js';
12
12
  import { applyHealChanges, collectProposals } from './heal.js';
13
13
  import { healPrecondition } from './heal-precondition.js';
14
14
  import { assertSuccess, dynamicTestids, successLabel } from './flow-success.js';
15
- import { flowErrorMessage, sessionProjectId } from './flow-replay-run.js';
15
+ import { flowErrorMessage } from './flow-replay-run.js';
16
+ import { sessionTarget } from '../../memory/project/session-root.js';
16
17
  import { flowsForSession } from './flow-store-for-session.js';
17
18
  const HEAL_MESSAGES = {
18
19
  NOTHING: 'nothing to heal — every anchor resolved on replay',
@@ -48,8 +49,9 @@ function toChange(proposal) {
48
49
  export async function healFlow(deps, args) {
49
50
  const name = asString(args['flowName']) ?? '';
50
51
  const apply = true === args['apply'];
51
- const projectId = sessionProjectId(deps, asString(args['sessionId']));
52
- const loaded = await flowsForSession(deps, projectId).flows.load(name, projectId);
52
+ const target = sessionTarget(deps, asString(args['sessionId']));
53
+ const { projectId } = target;
54
+ const loaded = await flowsForSession(deps, target).flows.load(name, projectId);
53
55
  if (!loaded.ok) {
54
56
  return {
55
57
  name,
@@ -163,7 +165,7 @@ export async function healFlow(deps, args) {
163
165
  }
164
166
  // Healing WRITES, so it must land where the load came from — a heal that read the app's flow and
165
167
  // wrote the daemon's copy would silently fork the two.
166
- const written = await flowsForSession(deps, projectId).flows.heal(name, proposals.map(toChange), projectId);
168
+ const written = await flowsForSession(deps, target).flows.heal(name, proposals.map(toChange), projectId);
167
169
  if (!written.ok) {
168
170
  return {
169
171
  name,
@@ -31,17 +31,17 @@ export declare const FlakeFileSchema: z.ZodObject<{
31
31
  fails: number;
32
32
  }>>;
33
33
  }, "strip", z.ZodTypeAny, {
34
- version: 1;
35
34
  flows: Record<string, {
36
35
  runs: number;
37
36
  fails: number;
38
37
  }>;
39
- }, {
40
38
  version: 1;
39
+ }, {
41
40
  flows: Record<string, {
42
41
  runs: number;
43
42
  fails: number;
44
43
  }>;
44
+ version: 1;
45
45
  }>;
46
46
  export declare function emptyRecord(): FlakeRecord;
47
47
  /** Accrue one unchanged-code replay outcome. */
@@ -33,9 +33,21 @@ export interface RecordedStep {
33
33
  endPage?: string;
34
34
  /** Why the agent took this step: the `intent` it declared on the action. Names the saved flow. */
35
35
  intent?: string;
36
+ /**
37
+ * Why a consequence this step PROVED is not in the flow: the flow file could not express it (a
38
+ * session ref such as `e12` means nothing on a later page). Said at stop, so a weaker flow is
39
+ * never saved silently.
40
+ */
41
+ unkeptExpect?: string;
36
42
  }
37
43
  interface ActiveRecording {
38
44
  cursor: number;
45
+ /**
46
+ * The session (tab) that started it. Only that session's steps join it: one daemon serves every
47
+ * agent and app on the machine, and capturing into every open recording put a click from an
48
+ * unrelated app into somebody else's saved flow (#988). Absent: takes every session's steps.
49
+ */
50
+ session?: string;
39
51
  steps: RecordedStep[];
40
52
  /** The route the journey began on. See CompiledProgram.startPath. */
41
53
  startPath?: string;
@@ -91,7 +103,10 @@ export declare const INVOKE_TOOL: string;
91
103
  export declare const AMBIENT_RECORDING = "__ambient__";
92
104
  export declare class RecordingStore {
93
105
  #private;
94
- start(name: string, cursor: number, startPath?: string): void;
106
+ private readonly byHarness;
107
+ /** `byHarness`: whether the step being captured was driven by the Harness, which tapes its own. */
108
+ constructor(byHarness?: () => boolean);
109
+ start(name: string, cursor: number, startPath?: string, session?: string): void;
95
110
  isRecording(name: string): boolean;
96
111
  /**
97
112
  * Number of steps captured so far in the named ACTIVE recording (0 if it
@@ -112,7 +127,7 @@ export declare class RecordingStore {
112
127
  * Opened lazily on the first step rather than in the constructor: a store that never records
113
128
  * anything should not carry an empty tape, and "did anything happen at all" stays answerable.
114
129
  */
115
- capture(step: RecordedStep, route?: string): void;
130
+ capture(step: RecordedStep, route?: string, session?: string): void;
116
131
  /**
117
132
  * Add a consequence to the last captured step of every active recording.
118
133
  *
@@ -120,20 +135,31 @@ export declare class RecordingStore {
120
135
  * reached the recorder — only the act tools called `capture` — so the flow kept the click and lost
121
136
  * the proof. What the step already declared is kept; the new check joins it under `allOf`.
122
137
  */
123
- attachExpect(expect: Predicate): void;
138
+ attachExpect(expect: Predicate, session?: string): void;
124
139
  /**
125
140
  * A navigation happened. Navigating records no step, so until the next one is captured, a check
126
141
  * proved now is about the page the navigation opened, not about the last step — and kept on that
127
142
  * step, replay would ask it right after the click, of a page it was never true on.
128
143
  */
129
144
  markNavigated(): void;
145
+ /**
146
+ * Take the expectation off the ambient tape's last step, because the verdict on it came back no.
147
+ *
148
+ * A step is captured at dispatch, before anything is judged, so a claim that turned out false is
149
+ * already on the tape as that step's `expect`. Cut into a journey it becomes a flow that is red for
150
+ * ever, on an expectation nobody but the agent's wrong guess made. The failure is not lost: it is
151
+ * filed as a capsule, the artifact built for "this should hold and does not". The ACTION stays,
152
+ * since the steps after it ran on the page it produced. A recording somebody opened deliberately
153
+ * is left as they drove it.
154
+ */
155
+ unassertLast(): void;
130
156
  /**
131
157
  * The page the step just captured ended on, once its action settled.
132
158
  *
133
159
  * Fills only a step that has none, so calling it after an action that captured nothing leaves the
134
160
  * previous step's own answer alone.
135
161
  */
136
- markEnded(page: string | undefined): void;
162
+ markEnded(page: string | undefined, session?: string): void;
137
163
  /**
138
164
  * Returns the active recording (cursor + steps) and clears it, or undefined if not recording.
139
165
  *
@@ -29,21 +29,31 @@ export const AMBIENT_RECORDING = '__ambient__';
29
29
  */
30
30
  const AMBIENT_STEP_CAP = 400;
31
31
  export class RecordingStore {
32
+ byHarness;
32
33
  #active = new Map();
33
34
  #compiled = new Map();
34
35
  /** A navigation came after the last captured step — see markNavigated. */
35
36
  #navigatedSinceStep = false;
36
- start(name, cursor, startPath) {
37
+ /** `byHarness`: whether the step being captured was driven by the Harness, which tapes its own. */
38
+ constructor(byHarness = () => false) {
39
+ this.byHarness = byHarness;
40
+ }
41
+ start(name, cursor, startPath, session) {
37
42
  const openedOver = new Map();
38
- for (const [outer, rec] of this.#active)
43
+ for (const [outer, rec] of this.#targets(session))
39
44
  openedOver.set(outer, rec.steps.length);
40
45
  this.#active.set(name, {
41
46
  cursor,
42
47
  steps: [],
43
48
  openedOver,
44
49
  ...(startPath === undefined ? {} : { startPath }),
50
+ ...(session === undefined ? {} : { session }),
45
51
  });
46
52
  }
53
+ /** The open recordings a step from `session` belongs to. See ActiveRecording.session. */
54
+ #targets(session) {
55
+ return [...this.#active].filter(([, rec]) => rec.session === undefined || session === undefined || rec.session === session);
56
+ }
47
57
  isRecording(name) {
48
58
  return this.#active.has(name);
49
59
  }
@@ -68,7 +78,7 @@ export class RecordingStore {
68
78
  * Opened lazily on the first step rather than in the constructor: a store that never records
69
79
  * anything should not carry an empty tape, and "did anything happen at all" stays answerable.
70
80
  */
71
- capture(step, route) {
81
+ capture(step, route, session) {
72
82
  this.#navigatedSinceStep = false;
73
83
  if (!this.#active.has(AMBIENT_RECORDING)) {
74
84
  this.#active.set(AMBIENT_RECORDING, {
@@ -77,7 +87,7 @@ export class RecordingStore {
77
87
  openedOver: new Map(),
78
88
  });
79
89
  }
80
- for (const [name, rec] of this.#active) {
90
+ for (const [name, rec] of this.#targets(session)) {
81
91
  // The ambient tape is opened by the system and closed by nobody, so it is the one recording
82
92
  // with no human deciding when it has seen enough. Bounded here rather than left to grow for
83
93
  // the length of a daemon's life. Appending STOPS at the cap instead of dropping the oldest: a
@@ -86,6 +96,10 @@ export class RecordingStore {
86
96
  // opened on purpose is not capped — they said when it starts and they say when it stops.
87
97
  if (AMBIENT_RECORDING === name && rec.steps.length >= AMBIENT_STEP_CAP)
88
98
  continue;
99
+ // The Harness saves its own recordings. Taping its steps too saved every drive twice: once as
100
+ // the Harness's flow and again, at session end, as a `drive-*` copy with a guessed name.
101
+ if (AMBIENT_RECORDING === name && this.byHarness())
102
+ continue;
89
103
  // A step nobody marked ended where the next one began.
90
104
  const previous = rec.steps.at(-1);
91
105
  if (previous !== undefined && previous.endPage === undefined && step.page !== undefined) {
@@ -104,10 +118,10 @@ export class RecordingStore {
104
118
  * reached the recorder — only the act tools called `capture` — so the flow kept the click and lost
105
119
  * the proof. What the step already declared is kept; the new check joins it under `allOf`.
106
120
  */
107
- attachExpect(expect) {
121
+ attachExpect(expect, session) {
108
122
  if (this.#navigatedSinceStep)
109
123
  return;
110
- for (const rec of this.#active.values()) {
124
+ for (const [, rec] of this.#targets(session)) {
111
125
  const last = rec.steps.at(-1);
112
126
  if (last === undefined)
113
127
  continue;
@@ -128,16 +142,34 @@ export class RecordingStore {
128
142
  markNavigated() {
129
143
  this.#navigatedSinceStep = true;
130
144
  }
145
+ /**
146
+ * Take the expectation off the ambient tape's last step, because the verdict on it came back no.
147
+ *
148
+ * A step is captured at dispatch, before anything is judged, so a claim that turned out false is
149
+ * already on the tape as that step's `expect`. Cut into a journey it becomes a flow that is red for
150
+ * ever, on an expectation nobody but the agent's wrong guess made. The failure is not lost: it is
151
+ * filed as a capsule, the artifact built for "this should hold and does not". The ACTION stays,
152
+ * since the steps after it ran on the page it produced. A recording somebody opened deliberately
153
+ * is left as they drove it.
154
+ */
155
+ unassertLast() {
156
+ const tape = this.#active.get(AMBIENT_RECORDING);
157
+ const last = tape?.steps.at(-1);
158
+ if (tape === undefined || last === undefined || last.expect === undefined)
159
+ return;
160
+ const { expect: _dropped, ...kept } = last;
161
+ tape.steps[tape.steps.length - 1] = kept;
162
+ }
131
163
  /**
132
164
  * The page the step just captured ended on, once its action settled.
133
165
  *
134
166
  * Fills only a step that has none, so calling it after an action that captured nothing leaves the
135
167
  * previous step's own answer alone.
136
168
  */
137
- markEnded(page) {
169
+ markEnded(page, session) {
138
170
  if (page === undefined)
139
171
  return;
140
- for (const rec of this.#active.values()) {
172
+ for (const [, rec] of this.#targets(session)) {
141
173
  const last = rec.steps.at(-1);
142
174
  if (last !== undefined && last.endPage === undefined)
143
175
  last.endPage = page;
@@ -0,0 +1,20 @@
1
+ import { ReticleCommand } from '@reticlehq/core';
2
+ import type { FlowReplayResult } from '@reticlehq/core';
3
+ import type { ToolDeps } from '../../surface/tools/tool-kit.js';
4
+ /** The session a HUD replay reports to. Resolved per message: a replay usually reloads the page. */
5
+ interface ProgressTarget {
6
+ pushNarration(text: string): void;
7
+ pushView(name: typeof ReticleCommand.FLOW_PROGRESS, args: object): void;
8
+ }
9
+ /** A human-facing one-liner for a panel replay verdict — ✓ passed / ⚠ drifted / ✗ errored / ? unverifiable. */
10
+ export declare function replayVerdictLine(result: FlowReplayResult): string;
11
+ /**
12
+ * Replay a saved flow because a person pressed ▶ in the HUD, with no agent involved.
13
+ *
14
+ * The chip they pressed shows the replay as it runs: a progress push after every step, and the
15
+ * verdict at the end, sent to whichever session holds the tab at that moment (a replay reloads the
16
+ * page, and the session object from before the reload no longer has a socket). The verdict also
17
+ * lands in the Agent Log as a line, where the "See the logs" link on the chip leads.
18
+ */
19
+ export declare function replayFromHud(deps: ToolDeps, resolve: (sessionId: string) => ProgressTarget | undefined, sessionId: string, flowName: string): Promise<void>;
20
+ export {};
@@ -0,0 +1,51 @@
1
+ import { FlowProgressStatus, ReplayStatus, ReticleCommand } from '@reticlehq/core';
2
+ import { replayAndLearn } from './flow-learning.js';
3
+ /** A human-facing one-liner for a panel replay verdict — ✓ passed / ⚠ drifted / ✗ errored / ? unverifiable. */
4
+ export function replayVerdictLine(result) {
5
+ if (result.status === ReplayStatus.OK)
6
+ return `✓ "${result.name}" passed`;
7
+ if (result.status === ReplayStatus.DRIFT)
8
+ return `⚠ "${result.name}" drifted — a step no longer matches`;
9
+ if (result.status === ReplayStatus.UNVERIFIABLE)
10
+ return `? "${result.name}" unverifiable — ${result.unverifiable?.reason ?? 'could not be graded'}`;
11
+ return `✗ "${result.name}" failed — ${result.error?.message ?? 'could not replay'}`;
12
+ }
13
+ /**
14
+ * Replay a saved flow because a person pressed ▶ in the HUD, with no agent involved.
15
+ *
16
+ * The chip they pressed shows the replay as it runs: a progress push after every step, and the
17
+ * verdict at the end, sent to whichever session holds the tab at that moment (a replay reloads the
18
+ * page, and the session object from before the reload no longer has a socket). The verdict also
19
+ * lands in the Agent Log as a line, where the "See the logs" link on the chip leads.
20
+ */
21
+ export async function replayFromHud(deps, resolve, sessionId, flowName) {
22
+ const progress = (done, total, status) => resolve(sessionId)?.pushView(ReticleCommand.FLOW_PROGRESS, {
23
+ name: flowName,
24
+ done,
25
+ total,
26
+ status,
27
+ });
28
+ resolve(sessionId)?.pushNarration(`▶ Replaying "${flowName}"…`);
29
+ progress(0, 0, FlowProgressStatus.PLAYING);
30
+ let total = 0;
31
+ let done = 0;
32
+ try {
33
+ const result = await replayAndLearn(deps, {
34
+ flowName,
35
+ sessionId,
36
+ onStep: (stepsDone, stepsTotal) => {
37
+ done = stepsDone;
38
+ total = stepsTotal;
39
+ progress(done, total, FlowProgressStatus.PLAYING);
40
+ },
41
+ });
42
+ const passed = ReplayStatus.OK === result.status;
43
+ progress(passed ? total : done, total, passed ? FlowProgressStatus.PASSED : FlowProgressStatus.FAILED);
44
+ resolve(sessionId)?.pushNarration(replayVerdictLine(result));
45
+ }
46
+ catch (error) {
47
+ const message = error instanceof Error ? error.message : String(error);
48
+ progress(done, total, FlowProgressStatus.FAILED);
49
+ resolve(sessionId)?.pushNarration(`✗ Replay "${flowName}" failed — ${message}`);
50
+ }
51
+ }
@@ -35,6 +35,8 @@ export declare function replayActionArgs(value: unknown, confirmDangerous?: bool
35
35
  * a hash-routed app makes meaningless in the document location — see routeOfEvent.
36
36
  */
37
37
  export declare function pathOf(url: string | undefined): string | undefined;
38
+ /** What a saved flow could not keep, and the fix. See RecordedStep.unkeptExpect. */
39
+ export declare const UNKEPT_EXPECT: string;
38
40
  /**
39
41
  * Capture an act into every in-flight recording, or do nothing when none is running.
40
42
  *
@@ -44,21 +46,23 @@ export declare function pathOf(url: string | undefined): string | undefined;
44
46
  */
45
47
  export declare function captureAct(recordings: {
46
48
  active: () => string[];
47
- capture: (step: RecordedStep, route?: string) => void;
49
+ capture: (step: RecordedStep, route?: string, session?: string) => void;
48
50
  }, args: Record<string, unknown>, res: unknown,
49
51
  /** Where this step ran, so the ambient tape can be cut into journeys. See RecordedStep.route. */
50
- route?: string): void;
52
+ route?: string,
53
+ /** The session that acted: only recordings it started take the step. */
54
+ session?: string): void;
51
55
  /**
52
56
  * Capture an act_and_wait once its verdict is known, keeping `until` as the step's expectation only
53
57
  * on a yes. Recorded before the verdict, a step kept an `until` that came back `no` or `no-fault` —
54
58
  * a regression test asserting something never once observed to hold because of the action. The
55
59
  * action itself is always recorded: it happened, and replay needs it to reach the next step.
56
60
  */
57
- export declare function captureVerdictedAct(recordings: Parameters<typeof captureAct>[0], args: Record<string, unknown>, res: unknown, route: string | undefined, verified: string | undefined): void;
61
+ export declare function captureVerdictedAct(recordings: Parameters<typeof captureAct>[0], args: Record<string, unknown>, res: unknown, route: string | undefined, verified: string | undefined, session?: string): void;
58
62
  /** Fold a PASSING standalone assertion into the step it proved. See RecordingStore.attachExpect. */
59
63
  export declare function captureAssertion(recordings: {
60
- attachExpect: (expect: Predicate) => void;
61
- }, raw: unknown): void;
64
+ attachExpect: (expect: Predicate, session?: string) => void;
65
+ }, raw: unknown, session?: string): void;
62
66
  /**
63
67
  * Compile a single reticle_act invocation into a normalized RecordedStep using the action result. Anchor
64
68
  * priority mirrors synthesizeAnchor: a data-testid is the gold standard; failing that, the element's