@reticlehq/server 3.3.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (326) hide show
  1. package/LICENSE-ENTERPRISE +1 -1
  2. package/README.md +3 -3
  3. package/SKILL.md +12 -14
  4. package/dist/chromium-launch-options.d.ts +4 -3
  5. package/dist/chromium-launch-options.js +6 -2
  6. package/dist/command/cli/adhoc-suite.d.ts +22 -0
  7. package/dist/command/cli/adhoc-suite.js +90 -19
  8. package/dist/command/cli/adhoc-verdict.d.ts +35 -0
  9. package/dist/command/cli/adhoc-verdict.js +90 -3
  10. package/dist/command/cli/answers/gate-hook-message.d.ts +6 -0
  11. package/dist/command/cli/answers/gate-hook-message.js +6 -0
  12. package/dist/command/cli/auth/cloud-keystore.d.ts +0 -2
  13. package/dist/command/cli/auth/cloud-keystore.js +0 -3
  14. package/dist/command/cli/auth/cloud-session.d.ts +6 -2
  15. package/dist/command/cli/auth/cloud-session.js +2 -0
  16. package/dist/command/cli/auth/device-flow.d.ts +2 -2
  17. package/dist/command/cli/cli-doctor.js +7 -5
  18. package/dist/command/cli/cli-flow-commands.d.ts +3 -6
  19. package/dist/command/cli/cli-flow-commands.js +67 -4
  20. package/dist/command/cli/cli-parse.d.ts +8 -1
  21. package/dist/command/cli/cli-parse.js +24 -4
  22. package/dist/command/cli/cli-update-commands.js +8 -1
  23. package/dist/command/cli/cli-verify.d.ts +24 -3
  24. package/dist/command/cli/cli-verify.js +54 -11
  25. package/dist/command/cli/cloud-cli.js +64 -2
  26. package/dist/command/cli/cloud-kit.js +3 -5
  27. package/dist/command/cli/cloud-login.d.ts +1 -1
  28. package/dist/command/cli/cloud-login.js +27 -8
  29. package/dist/command/cli/demo/demo-page.js +6 -3
  30. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +5 -0
  31. package/dist/command/cli/doctor/browser/chromium-hint.js +5 -0
  32. package/dist/command/cli/launch/cli-launch.d.ts +8 -0
  33. package/dist/command/cli/launch/cli-launch.js +21 -9
  34. package/dist/command/cli/launch/sdk-version-match.d.ts +84 -0
  35. package/dist/command/cli/launch/sdk-version-match.js +206 -0
  36. package/dist/command/cli/lifecycle/daemon-lifecycle.js +2 -16
  37. package/dist/command/cli/ports/resolve/cli-port.d.ts +25 -0
  38. package/dist/command/cli/ports/resolve/cli-port.js +125 -15
  39. package/dist/command/cli/setup-mcp-cli.js +21 -13
  40. package/dist/command/cli/tutorial.d.ts +2 -2
  41. package/dist/command/cli/tutorial.js +21 -7
  42. package/dist/command/cli.js +79 -11
  43. package/dist/command/daemon/binding/daemon-status-probe.d.ts +14 -2
  44. package/dist/command/daemon/binding/daemon-status-probe.js +34 -6
  45. package/dist/command/daemon/daemon-resolve.d.ts +9 -0
  46. package/dist/command/daemon/daemon-resolve.js +1 -1
  47. package/dist/command/daemon/lifetime/daemon-usefulness.d.ts +7 -1
  48. package/dist/command/daemon/lifetime/daemon-usefulness.js +30 -7
  49. package/dist/command/dev/stale-issue-guard.d.ts +3 -4
  50. package/dist/command/dev/stale-issue-guard.js +8 -8
  51. package/dist/command/setup/bringup/connect-lease.d.ts +17 -0
  52. package/dist/command/setup/bringup/connect-lease.js +38 -0
  53. package/dist/command/setup/bringup/dev-server-files.d.ts +68 -0
  54. package/dist/command/setup/bringup/dev-server-files.js +131 -0
  55. package/dist/command/setup/bringup/dev-server-log-cap.d.ts +35 -0
  56. package/dist/command/setup/bringup/dev-server-log-cap.js +121 -0
  57. package/dist/command/setup/bringup/dev-server-wait.d.ts +2 -0
  58. package/dist/command/setup/bringup/dev-server-wait.js +1 -1
  59. package/dist/command/setup/bringup/ensure-daemon.d.ts +65 -4
  60. package/dist/command/setup/bringup/ensure-daemon.js +135 -16
  61. package/dist/command/setup/bringup/static-page-server.d.ts +35 -0
  62. package/dist/command/setup/bringup/static-page-server.js +151 -0
  63. package/dist/command/setup/desktop-shape.d.ts +4 -0
  64. package/dist/command/setup/desktop-shape.js +18 -2
  65. package/dist/command/setup/init/init-port.d.ts +15 -0
  66. package/dist/command/setup/init/init-port.js +44 -0
  67. package/dist/command/setup/init/init-runtime.d.ts +1 -1
  68. package/dist/command/setup/init/init-runtime.js +91 -9
  69. package/dist/command/setup/node-effects.d.ts +27 -14
  70. package/dist/command/setup/node-effects.js +86 -45
  71. package/dist/command/setup/probe/loopback-probe-urls.d.ts +7 -0
  72. package/dist/command/setup/probe/loopback-probe-urls.js +40 -0
  73. package/dist/command/setup/probe/page-probe.d.ts +13 -2
  74. package/dist/command/setup/probe/page-probe.js +18 -3
  75. package/dist/command/setup/run-setup.d.ts +28 -1
  76. package/dist/command/setup/run-setup.js +103 -15
  77. package/dist/command/setup/session-pick.d.ts +13 -0
  78. package/dist/command/setup/session-pick.js +20 -0
  79. package/dist/command/setup/setup-command.d.ts +2 -14
  80. package/dist/command/setup/setup-command.js +105 -31
  81. package/dist/command/setup/setup-mcp.d.ts +2 -0
  82. package/dist/command/setup/setup-mcp.js +17 -8
  83. package/dist/command/setup/terminal/wait-progress.d.ts +8 -0
  84. package/dist/command/setup/terminal/wait-progress.js +17 -0
  85. package/dist/command/version/peer-announce.d.ts +6 -2
  86. package/dist/command/version/peer-announce.js +8 -6
  87. package/dist/command/version/version-nudge.d.ts +17 -3
  88. package/dist/command/version/version-nudge.js +25 -4
  89. package/dist/features/crawl/crawl-empty.js +1 -1
  90. package/dist/features/crawl/crawl-tools.d.ts +6 -0
  91. package/dist/features/crawl/crawl-tools.js +57 -0
  92. package/dist/features/crawl/crawl.js +171 -157
  93. package/dist/features/exhaust/app-coverage.d.ts +33 -0
  94. package/dist/features/exhaust/app-coverage.js +31 -0
  95. package/dist/features/exhaust/code-coverage.d.ts +47 -0
  96. package/dist/features/exhaust/code-coverage.js +77 -0
  97. package/dist/features/exhaust/exhaust-run.d.ts +33 -0
  98. package/dist/features/exhaust/exhaust-run.js +82 -0
  99. package/dist/features/exhaust/explorer.d.ts +103 -0
  100. package/dist/features/exhaust/explorer.js +195 -0
  101. package/dist/features/exhaust/failure-branches.d.ts +33 -0
  102. package/dist/features/exhaust/failure-branches.js +92 -0
  103. package/dist/features/exhaust/ledger.d.ts +202 -0
  104. package/dist/features/exhaust/ledger.js +252 -0
  105. package/dist/features/exhaust/session-fold.d.ts +31 -0
  106. package/dist/features/exhaust/session-fold.js +79 -0
  107. package/dist/features/exhaust/session-port.d.ts +36 -0
  108. package/dist/features/exhaust/session-port.js +102 -0
  109. package/dist/features/harness/drive-report.d.ts +0 -9
  110. package/dist/features/harness/drive-report.js +18 -0
  111. package/dist/features/harness/driver.js +4 -7
  112. package/dist/features/harness/fill-values.d.ts +10 -0
  113. package/dist/features/harness/fill-values.js +50 -11
  114. package/dist/features/harness/jev-driver.js +30 -13
  115. package/dist/features/harness/openai-driver.d.ts +12 -0
  116. package/dist/features/harness/openai-driver.js +21 -12
  117. package/dist/features/harness/platform-config.js +5 -6
  118. package/dist/features/license/license.d.ts +2 -2
  119. package/dist/index.d.ts +9 -0
  120. package/dist/index.js +56 -19
  121. package/dist/judgement/capsule/capsule-store.d.ts +4 -4
  122. package/dist/judgement/domain/domain-model.js +3 -1
  123. package/dist/judgement/oracles/self-instrument.js +4 -2
  124. package/dist/judgement/runs/artifact/replay-mapping.d.ts +2 -2
  125. package/dist/judgement/runs/artifact/replay-mapping.js +44 -9
  126. package/dist/judgement/runs/verification-sync.d.ts +3 -1
  127. package/dist/judgement/runs/verification-sync.js +1 -1
  128. package/dist/language/flows/change/affected.d.ts +9 -0
  129. package/dist/language/flows/change/affected.js +35 -0
  130. package/dist/language/flows/change/gate.d.ts +20 -0
  131. package/dist/language/flows/change/gate.js +12 -1
  132. package/dist/language/flows/flow-author.d.ts +7 -0
  133. package/dist/language/flows/flow-author.js +19 -0
  134. package/dist/language/flows/flow-expect-grammar.js +1 -1
  135. package/dist/language/flows/flow-journey.d.ts +54 -0
  136. package/dist/language/flows/flow-journey.js +119 -0
  137. package/dist/language/flows/flow-replay-run.d.ts +2 -3
  138. package/dist/language/flows/flow-replay-run.js +13 -47
  139. package/dist/language/flows/flow-replay.d.ts +22 -8
  140. package/dist/language/flows/flow-replay.js +55 -9
  141. package/dist/language/flows/flow-result.d.ts +2 -0
  142. package/dist/language/flows/flow-result.js +25 -0
  143. package/dist/language/flows/flow-tools.js +17 -5
  144. package/dist/language/flows/flows.d.ts +10 -1
  145. package/dist/language/flows/flows.js +90 -14
  146. package/dist/language/flows/recording/replay-halt.js +3 -1
  147. package/dist/language/flows/recording/tape/recordings.d.ts +30 -1
  148. package/dist/language/flows/recording/tape/recordings.js +55 -1
  149. package/dist/language/flows/replay.d.ts +19 -0
  150. package/dist/language/flows/replay.js +42 -6
  151. package/dist/language/flows/verify-change-tools.js +5 -2
  152. package/dist/launch-chromium.d.ts +80 -0
  153. package/dist/launch-chromium.js +218 -0
  154. package/dist/memory/cloud/cloud-config.d.ts +11 -2
  155. package/dist/memory/cloud/cloud-config.js +47 -7
  156. package/dist/memory/cloud/cloud-sync.d.ts +4 -1
  157. package/dist/memory/cloud/cloud-sync.js +6 -9
  158. package/dist/memory/cloud/harness-offer.js +5 -6
  159. package/dist/memory/cloud/harness-switch.js +5 -6
  160. package/dist/memory/cloud/sync-cycle.d.ts +77 -3
  161. package/dist/memory/cloud/sync-cycle.js +325 -55
  162. package/dist/memory/cloud/sync-daemon.js +24 -20
  163. package/dist/memory/cloud/sync-disk.js +16 -2
  164. package/dist/memory/intent/intent-shard.d.ts +10 -32
  165. package/dist/memory/intent/intent-shard.js +0 -10
  166. package/dist/memory/intent/intent-tools.js +8 -3
  167. package/dist/memory/journal/drive-flow.d.ts +18 -13
  168. package/dist/memory/journal/drive-flow.js +45 -37
  169. package/dist/memory/journal/envelope.d.ts +8 -8
  170. package/dist/memory/journal/journal-query.d.ts +14 -0
  171. package/dist/memory/journal/journal-query.js +15 -0
  172. package/dist/memory/journal/on-disk/workspace-tiers.js +2 -0
  173. package/dist/memory/journal/session-end.d.ts +9 -1
  174. package/dist/memory/journal/session-end.js +24 -12
  175. package/dist/memory/project/artifact-root-resolver.d.ts +10 -0
  176. package/dist/memory/project/artifact-root-resolver.js +46 -19
  177. package/dist/memory/project/dir/reticle-dir.d.ts +2 -0
  178. package/dist/memory/project/dir/reticle-dir.js +1 -0
  179. package/dist/memory/project/project-tools.js +3 -1
  180. package/dist/portal/bridge/auth-failure-reason.d.ts +1 -1
  181. package/dist/portal/bridge/auth-failure-reason.js +4 -2
  182. package/dist/portal/bridge/bridge.d.ts +12 -3
  183. package/dist/portal/bridge/bridge.js +49 -3
  184. package/dist/portal/input/js-coverage.d.ts +25 -0
  185. package/dist/portal/input/js-coverage.js +30 -0
  186. package/dist/portal/input/real-input.d.ts +11 -0
  187. package/dist/portal/input/real-input.js +29 -4
  188. package/dist/portal/input/viewport-tools.js +16 -6
  189. package/dist/portal/pool/playwright-launcher.d.ts +6 -0
  190. package/dist/portal/pool/playwright-launcher.js +20 -13
  191. package/dist/portal/session/dev-server/dev-command.d.ts +2 -2
  192. package/dist/portal/session/dev-server/dev-command.js +30 -12
  193. package/dist/portal/session/dev-server/dev-server-probe.js +17 -1
  194. package/dist/portal/session/dev-server/route-status-probe.d.ts +24 -0
  195. package/dist/portal/session/dev-server/route-status-probe.js +57 -0
  196. package/dist/portal/session/facts/observed-state.d.ts +11 -0
  197. package/dist/portal/session/facts/observed-state.js +34 -0
  198. package/dist/portal/session/no-session-diagnosis.d.ts +29 -0
  199. package/dist/portal/session/no-session-diagnosis.js +62 -5
  200. package/dist/portal/session/no-session-next-action.d.ts +13 -0
  201. package/dist/portal/session/no-session-next-action.js +48 -9
  202. package/dist/portal/session/no-session-watch.d.ts +18 -0
  203. package/dist/portal/session/no-session-watch.js +88 -2
  204. package/dist/portal/session/session-health.js +3 -2
  205. package/dist/portal/session/session-manager.d.ts +20 -6
  206. package/dist/portal/session/session-manager.js +19 -3
  207. package/dist/portal/session/session-reaper.d.ts +28 -0
  208. package/dist/portal/session/session-reaper.js +49 -1
  209. package/dist/portal/session/session.d.ts +10 -8
  210. package/dist/portal/session/session.js +24 -32
  211. package/dist/surface/http-server.d.ts +3 -2
  212. package/dist/surface/http-server.js +8 -5
  213. package/dist/surface/mcp/mcp-proxy.d.ts +2 -2
  214. package/dist/surface/mcp/mcp-proxy.js +2 -2
  215. package/dist/surface/mcp/mcp.js +5 -3
  216. package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +13 -1
  217. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +27 -2
  218. package/dist/surface/mcp/proxy/proxy-log.js +3 -5
  219. package/dist/surface/mcp/server-instructions.js +9 -7
  220. package/dist/surface/tools/act/after-match.d.ts +17 -0
  221. package/dist/surface/tools/act/after-match.js +30 -0
  222. package/dist/surface/tools/act/durable.d.ts +69 -0
  223. package/dist/surface/tools/act/durable.js +139 -0
  224. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +13 -6
  225. package/dist/surface/tools/act/navigation/navigate-arrival.js +47 -10
  226. package/dist/surface/tools/act/navigation/navigate-result.d.ts +1 -12
  227. package/dist/surface/tools/act/navigation/navigate-result.js +21 -3
  228. package/dist/surface/tools/act/proved-controls.d.ts +9 -0
  229. package/dist/surface/tools/act/proved-controls.js +11 -0
  230. package/dist/surface/tools/act/sequence-grade.d.ts +5 -0
  231. package/dist/surface/tools/act/sequence-grade.js +32 -1
  232. package/dist/surface/tools/act-capsule.d.ts +7 -0
  233. package/dist/surface/tools/act-capsule.js +46 -1
  234. package/dist/surface/tools/act-sequence-tool.js +25 -8
  235. package/dist/surface/tools/act-tools.js +28 -52
  236. package/dist/surface/tools/assert/assert-grade.js +12 -1
  237. package/dist/surface/tools/assert/assert-verdict.js +2 -2
  238. package/dist/surface/tools/browser-tools.js +14 -2
  239. package/dist/surface/tools/coverage-tools.js +27 -1
  240. package/dist/surface/tools/dynamic-tools.js +29 -2
  241. package/dist/surface/tools/error-recovery.js +5 -6
  242. package/dist/surface/tools/feedback-invite.js +6 -4
  243. package/dist/surface/tools/harness-explore.d.ts +3 -2
  244. package/dist/surface/tools/harness-explore.js +12 -7
  245. package/dist/surface/tools/invoke-tool.js +12 -8
  246. package/dist/surface/tools/lease-tools.js +15 -6
  247. package/dist/surface/tools/live-call-text.js +4 -1
  248. package/dist/surface/tools/observe-tools.js +9 -1
  249. package/dist/surface/tools/tool-kit.d.ts +10 -0
  250. package/dist/surface/tools/tools.js +4 -0
  251. package/dist/telemetry/argument-shape.d.ts +0 -2
  252. package/dist/telemetry/argument-shape.js +1 -5
  253. package/dist/telemetry/error-class.d.ts +2 -3
  254. package/dist/telemetry/error-class.js +2 -3
  255. package/dist/telemetry/harness-drive.d.ts +0 -2
  256. package/dist/telemetry/harness-drive.js +0 -4
  257. package/dist/telemetry/onboarding-firsts.d.ts +2 -2
  258. package/dist/telemetry/onboarding-firsts.js +2 -2
  259. package/dist/wire-journal.d.ts +3 -0
  260. package/dist/wire-journal.js +4 -0
  261. package/dist/workspace-packages.d.ts +0 -2
  262. package/dist/workspace-packages.js +1 -9
  263. package/docs/README.md +4 -2
  264. package/docs/actions.mdx +3 -4
  265. package/docs/agent-cheatsheet.md +4 -4
  266. package/docs/architecture.md +6 -6
  267. package/docs/best-practices.mdx +1 -1
  268. package/docs/capabilities.mdx +32 -1
  269. package/docs/cli/cloud.mdx +20 -8
  270. package/docs/cli/doctor.mdx +4 -0
  271. package/docs/cli/feedback.mdx +2 -2
  272. package/docs/cli/gate.mdx +9 -4
  273. package/docs/cli/update.mdx +10 -0
  274. package/docs/cli.mdx +22 -7
  275. package/docs/desktop.mdx +14 -10
  276. package/docs/environment.mdx +87 -0
  277. package/docs/faq.mdx +18 -16
  278. package/docs/first-drive.md +5 -13
  279. package/docs/flows.md +21 -2
  280. package/docs/frameworks.mdx +29 -10
  281. package/docs/getting-started.md +15 -5
  282. package/docs/http-transport.md +6 -3
  283. package/docs/human-control.md +2 -2
  284. package/docs/index.mdx +16 -6
  285. package/docs/install-agentic.mdx +2 -2
  286. package/docs/install-manual.mdx +1 -1
  287. package/docs/integration-patterns.md +3 -3
  288. package/docs/multi-agent-testing.md +1 -1
  289. package/docs/packages/babel-plugin.mdx +1 -1
  290. package/docs/packages/browser.mdx +2 -2
  291. package/docs/packages/next.mdx +3 -0
  292. package/docs/packages/react.mdx +2 -2
  293. package/docs/packages/tauri.mdx +1 -1
  294. package/docs/packages/vite-plugin.mdx +4 -2
  295. package/docs/packages.mdx +9 -9
  296. package/docs/predicates.mdx +50 -5
  297. package/docs/quickstart.mdx +12 -10
  298. package/docs/recipes.mdx +2 -2
  299. package/docs/state-management.mdx +2 -2
  300. package/docs/system-map.md +1 -1
  301. package/docs/telemetry.md +1 -1
  302. package/docs/token-efficiency.md +3 -3
  303. package/docs/tools/act-and-wait.mdx +1 -0
  304. package/docs/tools/act-sequence.mdx +1 -1
  305. package/docs/tools/act.mdx +1 -1
  306. package/docs/tools/console.mdx +1 -1
  307. package/docs/tools/inspect.mdx +1 -1
  308. package/docs/tools/network.mdx +1 -1
  309. package/docs/tools/query.mdx +1 -1
  310. package/docs/tools/sessions.mdx +1 -1
  311. package/docs/tools/snapshot.mdx +1 -1
  312. package/docs/tools/state.mdx +1 -1
  313. package/docs/tools/wait-for.mdx +1 -1
  314. package/docs/usage.md +7 -7
  315. package/docs/use-cases.mdx +157 -0
  316. package/docs/vs-screenshots.mdx +3 -3
  317. package/docs/what-is-recorded.md +2 -2
  318. package/docs/what-is-reticle.mdx +17 -20
  319. package/docs/why-reticle.mdx +14 -21
  320. package/package.json +10 -9
  321. package/dist/judgement/capsule/minimize.d.ts +0 -12
  322. package/dist/judgement/capsule/minimize.js +0 -22
  323. package/dist/judgement/phenomena/phenomena.d.ts +0 -27
  324. package/dist/judgement/phenomena/phenomena.js +0 -101
  325. package/dist/language/flows/flow-report.d.ts +0 -22
  326. package/dist/language/flows/flow-report.js +0 -106
@@ -0,0 +1,119 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { PredicateKind } from '@reticlehq/core';
3
+ /**
4
+ * One journey, however many times it was driven.
5
+ *
6
+ * Every browser session saves its own flow at teardown, so a journey driven in five sessions was
7
+ * five near-identical files. Two flows are the SAME journey when they start on the same page and
8
+ * take the same steps: same tool, action, anchor and invocation, in the same order. What was typed
9
+ * and what was claimed do not decide it — a re-drive that fills a different value or declares one
10
+ * more consequence is the same journey, better described.
11
+ */
12
+ /** The anchor minus its source location, which moves when an unrelated line is edited. */
13
+ function anchorShape(anchor) {
14
+ const { source: _source, ...rest } = anchor;
15
+ return rest;
16
+ }
17
+ function stepShape(step) {
18
+ return [
19
+ step.tool,
20
+ step.action ?? null,
21
+ anchorShape(step.anchor),
22
+ step.invoke ?? null,
23
+ (step.steps ?? []).map(stepShape),
24
+ ];
25
+ }
26
+ const FINGERPRINT_LENGTH = 12;
27
+ /** A short, stable identity for a journey's shape: its start page and its steps. */
28
+ export function journeyFingerprint(flow) {
29
+ return createHash('sha256')
30
+ .update(JSON.stringify([flow.startPath ?? null, flow.steps.map(stepShape)]))
31
+ .digest('hex')
32
+ .slice(0, FINGERPRINT_LENGTH);
33
+ }
34
+ /** `kept` wins wherever it says something; `other` fills only what `kept` left unsaid. */
35
+ function fillStep(kept, other) {
36
+ if (other === undefined)
37
+ return kept;
38
+ const merged = { ...other, ...kept };
39
+ if (kept.steps !== undefined) {
40
+ merged.steps = kept.steps.map((sub, i) => fillStep(sub, other.steps?.[i]));
41
+ }
42
+ return merged;
43
+ }
44
+ /**
45
+ * Fold two copies of one journey into one file.
46
+ *
47
+ * Gap-filling, never overwriting: whatever the kept flow already says — its name, its intent, a
48
+ * step's declared consequence, a quarantine a person wrote — survives, and the other copy only
49
+ * supplies what is missing. So a merge can add an assertion or an intent and can never remove one.
50
+ */
51
+ export function mergeJourney(kept, other) {
52
+ return {
53
+ ...other,
54
+ ...kept,
55
+ steps: kept.steps.map((step, i) => fillStep(step, other.steps[i])),
56
+ };
57
+ }
58
+ /**
59
+ * The name a new journey saves under.
60
+ *
61
+ * Its readable name when that is free or already this journey's; otherwise the same name with the
62
+ * fingerprint's head, so two different journeys that share an intent never overwrite each other.
63
+ */
64
+ export function journeyName(base, fingerprint, taken) {
65
+ const owner = taken.get(base);
66
+ return owner === undefined || owner === fingerprint ? base : `${base}-${fingerprint.slice(0, 6)}`;
67
+ }
68
+ /**
69
+ * What must hold before a journey and what it leaves behind, read from where it started and ended.
70
+ *
71
+ * Routes only: the page is the one piece of state every recorded step knows for certain, and a
72
+ * claim written from a guess about the rest would be a precondition nobody met. Written in the
73
+ * exact form `canFollow` compares by value, so A-then-B is answerable from the two files alone.
74
+ */
75
+ export function routeClaims(startPath, steps) {
76
+ const first = startPath ?? steps[0]?.page;
77
+ const last = steps.at(-1);
78
+ const end = last?.endPage ?? last?.page;
79
+ return {
80
+ ...(first === undefined ? {} : { requires: [{ kind: PredicateKind.ROUTE, pathname: first }] }),
81
+ ...(end === undefined ? {} : { ensures: [{ kind: PredicateKind.ROUTE, pathname: end }] }),
82
+ };
83
+ }
84
+ /**
85
+ * The preconditions only another flow can establish.
86
+ *
87
+ * A route claim on the page the flow starts on is not one of them: replay navigates there itself, so
88
+ * it neither opts the flow out of the reset nor gates the run. That is the claim a saved drive writes
89
+ * for the map (see routeClaims) — reading it as "starts from leftover state" would stop every
90
+ * auto-saved flow from ever being reset.
91
+ */
92
+ export function establishedState(flow) {
93
+ const start = flow.startPath;
94
+ return (flow.requires ?? []).filter((claim) => !(start !== undefined &&
95
+ PredicateKind.ROUTE === claim.kind &&
96
+ claim.pathname !== undefined &&
97
+ samePath(claim.pathname, start)));
98
+ }
99
+ /**
100
+ * Is the tab where the flow asked to start? Up to a trailing slash, and up to the query the flow
101
+ * did not ask about.
102
+ *
103
+ * `startPath` is the SPECIFICATION, so it decides what counts. A query it recorded is compared:
104
+ * `?tab=wrap` and `?tab=summary` are different pages, and a replay that starts on the wrong one
105
+ * proves nothing about the right one. A query it did NOT record is ignored: the tab carrying
106
+ * `?next=%2F` on a login page, or the identity params Reticle puts on a leased tab, are not the flow
107
+ * being elsewhere, and navigating to strip them costs a session for nothing.
108
+ *
109
+ * The asymmetry is the whole point and the reason `observed` and `expected` are named rather than
110
+ * `a` and `b`. Comparing with the query on both sides always re-navigated a query-bearing
111
+ * `startPath` (#1059, which killed the session mid-flow); comparing with it on neither side reads a
112
+ * tab on `?tab=summary` as already at `?tab=wrap`.
113
+ */
114
+ export function samePath(observed, expected) {
115
+ const trimmed = (path) => path.replace(/\/$/, '');
116
+ const withoutQuery = (path) => path.replace(/\?[^#]*/, '');
117
+ const comparable = expected.includes('?') ? observed : withoutQuery(observed);
118
+ return trimmed(comparable) === trimmed(expected);
119
+ }
@@ -1,4 +1,4 @@
1
- import { FlowErrorCode, type CommandResult, type FlowFile, type FlowReplayResult, type Contradiction, type FlowStepResult, type ReticleEvent } from '@reticlehq/core';
1
+ import { type CommandResult, type FlowFile, type FlowReplayResult, type Contradiction, type FlowStepResult, type ReticleEvent } from '@reticlehq/core';
2
2
  import type { ArrivalClock } from '../../surface/tools/act/navigation/navigate-arrival.js';
3
3
  import type { SessionManager } from '../../portal/session/session-manager.js';
4
4
  import type { Session } from '../../portal/session/session.js';
@@ -8,8 +8,7 @@ export declare function latestRecordedFlow(events: ReticleEvent[]): {
8
8
  name: string;
9
9
  flow: import('@reticlehq/core').FlowFile;
10
10
  } | undefined;
11
- /** Map a structured FlowErrorCode to a legible one-line message for the agent. */
12
- export declare function flowErrorMessage(code: FlowErrorCode, detail?: string): string;
11
+ export { flowErrorMessage } from './flow-result.js';
13
12
  /** The slice of a session the start-path logic reads: the live URL plus the event buffer. */
14
13
  interface StartPathSession {
15
14
  url?: string;
@@ -2,6 +2,7 @@ import { resolveFlowUploads } from './fields/flow-upload-resolve.js';
2
2
  import { learnFromRun } from '@reticlehq/engine/evidence/learned-guards.js';
3
3
  import { EventType, FLOW_SIGNAL_TIMEOUT_MS, FlowErrorCode, RecordedFlowSchema, ReplayStatus, ReticleCommand, RunKind, RunStatus, } from '@reticlehq/core';
4
4
  import { haltedFrom } from './recording/replay-halt.js';
5
+ import { establishedState, samePath } from './flow-journey.js';
5
6
  import { asRecord, asString } from '@reticlehq/core';
6
7
  import { routeOfEvent, routeOfUrl } from '@reticlehq/engine/question/predicate/predicate-route.js';
7
8
  import { carryReticleIdentity } from '../../surface/tools/lease-tools.js';
@@ -11,7 +12,7 @@ import { queryRefs } from './replay.js';
11
12
  import { assertSuccess, dynamicTestids, successLabel, SUCCESS_STEP_TOOL } from './flow-success.js';
12
13
  import { buildDecision, unverifiableReason } from './decision.js';
13
14
  import { unsuppliedSecrets } from './fields/flow-secret-field.js';
14
- import { assertStepExpect, DocumentLostDuringReplay, } from './flow-replay.js';
15
+ import { assertStepExpect, DocumentLostDuringReplay, resumeArg, unresolvedResume, } from './flow-replay.js';
15
16
  import { isDocumentGoneError } from '../../portal/session/facts/session-replaced.js';
16
17
  import { classifyFlowAssertions, flattenSteps } from './flow-classify.js';
17
18
  import { dischargeFlowIntent, flowIntentStatement, flowReplayVerdictId } from './flow-intent.js';
@@ -31,7 +32,6 @@ import { consultSubjectFor, selectConsulted } from './flow-memory-consult.js';
31
32
  import { log } from '../../log.js';
32
33
  import { flowsForSession } from './flow-store-for-session.js';
33
34
  import { projectForRoot } from '../../memory/project/project-for-root.js';
34
- import { FlowParseNote } from './flow-expect-grammar.js';
35
35
  export function latestRecordedFlow(events) {
36
36
  for (let i = events.length - 1; i >= 0; i--) {
37
37
  const event = events[i];
@@ -43,28 +43,8 @@ export function latestRecordedFlow(events) {
43
43
  }
44
44
  return undefined;
45
45
  }
46
- /** Map a structured FlowErrorCode to a legible one-line message for the agent. */
47
- export function flowErrorMessage(code, detail) {
48
- if (FlowErrorCode.PARSE_FAILED === code && undefined !== detail)
49
- return detail;
50
- // The detail names both versions and the remedy, so it beats anything generic this could say.
51
- if (FlowErrorCode.WRONG_VERSION === code && undefined !== detail)
52
- return detail;
53
- switch (code) {
54
- case FlowErrorCode.INVALID_NAME:
55
- return 'invalid flow name — use a single safe segment (letters/digits/-/_), no path separators';
56
- case FlowErrorCode.NOT_FOUND:
57
- return 'no such flow on disk — run reticle_flow{action:"list"} to see saved flows';
58
- case FlowErrorCode.PARSE_FAILED:
59
- return FlowParseNote.MALFORMED;
60
- case FlowErrorCode.NO_RECORDING:
61
- return 'no compiled recording by that name — record one (reticle_record{action:"start"|"stop"}) first';
62
- // Never "regenerate it": the file is intact and the reader is the wrong one. Telling somebody
63
- // to rewrite an undamaged flow is the failure this code was split out of PARSE_FAILED to stop.
64
- case FlowErrorCode.WRONG_VERSION:
65
- return 'this flow file was written in a different flow-file format — the file is not damaged, this Reticle cannot read that version. Upgrade or downgrade Reticle rather than editing the flow';
66
- }
67
- }
46
+ export { flowErrorMessage } from './flow-result.js';
47
+ import { flowErrorMessage } from './flow-result.js';
68
48
  /** Map the wire ReplayStatus onto the persisted RunStatus (ok→pass). */
69
49
  function replayToRunStatus(status) {
70
50
  switch (status) {
@@ -133,27 +113,6 @@ function currentPathOf(session) {
133
113
  const fromUrl = routeOfUrl(session.url);
134
114
  return fromUrl === undefined ? undefined : `${fromUrl.docPath}${fromUrl.search}${fromUrl.hash}`;
135
115
  }
136
- /**
137
- * Is the tab where the flow asked to start? Up to a trailing slash, and up to the query the flow
138
- * did not ask about.
139
- *
140
- * `startPath` is the SPECIFICATION, so it decides what counts. A query it recorded is compared:
141
- * `?tab=wrap` and `?tab=summary` are different pages, and a replay that starts on the wrong one
142
- * proves nothing about the right one. A query it did NOT record is ignored: the tab carrying
143
- * `?next=%2F` on a login page, or the identity params Reticle puts on a leased tab, are not the flow
144
- * being elsewhere, and navigating to strip them costs a session for nothing.
145
- *
146
- * The asymmetry is the whole point and the reason `observed` and `expected` are named rather than
147
- * `a` and `b`. Comparing with the query on both sides always re-navigated a query-bearing
148
- * `startPath` (#1059, which killed the session mid-flow); comparing with it on neither side reads a
149
- * tab on `?tab=summary` as already at `?tab=wrap`.
150
- */
151
- function samePath(observed, expected) {
152
- const trimmed = (path) => path.replace(/\/$/, '');
153
- const withoutQuery = (path) => path.replace(/\?[^#]*/, '');
154
- const comparable = expected.includes('?') ? observed : withoutQuery(observed);
155
- return trimmed(comparable) === trimmed(expected);
156
- }
157
116
  /**
158
117
  * Ask the project what it already knows about this flow.
159
118
  *
@@ -321,7 +280,7 @@ export async function arriveAtStartPath(sessions, session, flow, timeoutMs = STA
321
280
  if (current === undefined)
322
281
  return {};
323
282
  // The declared opt-out, both directions. See above.
324
- if (0 < (flow.requires?.length ?? 0))
283
+ if (0 < establishedState(flow).length)
325
284
  return {};
326
285
  const here = samePath(current, target);
327
286
  // A route mismatch that step 1 can start from anyway is not worth a page load: navigating away
@@ -457,7 +416,7 @@ async function loadInvokedFlows(deps, flow, projectId) {
457
416
  * a consequence of the first being absent.
458
417
  */
459
418
  async function firstUnmetPrecondition(session, flow, since) {
460
- for (const claim of flow.requires ?? []) {
419
+ for (const claim of establishedState(flow)) {
461
420
  // Zero budget: a precondition is a claim about the state you are starting FROM. Waiting for one
462
421
  // turns "was it true" into "did it become true", which is a different and much weaker question.
463
422
  let drift;
@@ -608,6 +567,12 @@ observe = (s) => s) {
608
567
  },
609
568
  };
610
569
  }
570
+ const from = resumeArg(args['from']);
571
+ const badResume = unresolvedResume(from, replayable.steps);
572
+ if (badResume !== undefined) {
573
+ const error = { code: FlowErrorCode.STEP_NOT_FOUND, message: badResume };
574
+ return { name: replayable.name, status: ReplayStatus.ERROR, steps: [], error };
575
+ }
611
576
  const unmet = await firstUnmetPrecondition(session, replayable, replayFloor);
612
577
  if (unmet !== undefined) {
613
578
  return {
@@ -629,6 +594,7 @@ observe = (s) => s) {
629
594
  // Bug-sweep mode: keep going past a step whose action ran and whose consequence merely did
630
595
  // not hold, so one flow reports one verdict per step instead of stopping at the first defect.
631
596
  sweep: true === args['sweep'],
597
+ ...(from === undefined ? {} : { from }),
632
598
  });
633
599
  }
634
600
  catch (error) {
@@ -1,4 +1,4 @@
1
- export { anchorLabel, componentLabel, componentQueryArgs, editDistance, expectElementDrift, nearestIsAmbiguous, nearestTestid, resolveQuery, testidDrift, } from './flow-anchor.js';
1
+ export { nearestIsAmbiguous, nearestTestid, resolveQuery } from './flow-anchor.js';
2
2
  import type { FlowReplaySession, WaitForSignal, Sleep } from './flow-replay-types.js';
3
3
  export type { FlowReplaySession, WaitForSignal, Sleep } from './flow-replay-types.js';
4
4
  import { type Drift, type FlowFile, type FlowStep, type FlowStepResult } from '@reticlehq/core';
@@ -50,15 +50,18 @@ export declare function assertStepExpect(session: FlowReplaySession, expect: Non
50
50
  * results. This is the "whose fault is it" contract, not a blind "command failed".
51
51
  */
52
52
  /**
53
- * How a replay is loaded and how far it goes — not WHERE it starts.
54
- *
55
- * It carried a `from` (and a `surface` that existed only to gate it): replay from step N, re-driving
56
- * the prefix silently. The machinery was built, tested and reachable from nothing at all — no tool
57
- * argument, no CLI flag, no caller anywhere. Deleted rather than wired: unreachable code is read as
58
- * current by the next person and maintained forever, its passing tests make it look load-bearing,
59
- * and nobody has asked for resume-from-step. The history keeps it if it is ever wanted.
53
+ * How a replay is loaded, where it starts reporting, and how far it goes.
60
54
  */
61
55
  export interface ReplayFromOptions {
56
+ /**
57
+ * Resume at this step: an index, or a step's `id` (which survives edits that shift indices).
58
+ *
59
+ * There is no state to restore, so the steps before it are re-driven — quickly, as setup: their
60
+ * actions run, their declared consequences are not checked and their results are not reported.
61
+ * The one exception is a setup step that FAILS: the resume never reached the step it was asked
62
+ * for, and swallowing that would report the run as starting where it did not.
63
+ */
64
+ from?: number | string;
62
65
  /**
63
66
  * How to load a flow this one INVOKES. Absent means invocations cannot be followed.
64
67
  *
@@ -89,4 +92,15 @@ export interface ReplayFromOptions {
89
92
  */
90
93
  sweep?: boolean;
91
94
  }
95
+ /**
96
+ * The index a replay resumes at, or `undefined` when `from` names no step of this flow.
97
+ *
98
+ * A step declared `commits` in the prefix refuses the resume (0 = replay everything): re-driving it
99
+ * is not setup, it is a second charge, a second email, a second record.
100
+ */
101
+ export declare function resumeIndex(from: number | string | undefined, steps: readonly FlowStep[]): number | undefined;
102
+ /** The `from` argument, narrowed: a step index or a step id. */
103
+ export declare function resumeArg(raw: unknown): number | string | undefined;
104
+ /** Why `from` cannot be resumed at, or undefined when it can (or was not given). */
105
+ export declare function unresolvedResume(from: number | string | undefined, steps: readonly FlowStep[]): string | undefined;
92
106
  export declare function replayFlow(session: FlowReplaySession, flow: FlowFile, waitForSignal: WaitForSignal, signalTimeoutMs: number, confirmDangerous?: boolean, sleep?: Sleep, options?: ReplayFromOptions): Promise<FlowStepResult[]>;
@@ -2,10 +2,10 @@ import { formatStepAddress } from 'open-verification';
2
2
  import { assertsState, expectLabel, expectedElementTestid, withoutClauses } from '@reticlehq/core';
3
3
  import { span } from '../../trace.js';
4
4
  import { anchorLabel, expectElementDrift, resolveTestid, testidDrift, ambiguousAnchorDrift, } from './flow-anchor.js';
5
- export { anchorLabel, componentLabel, componentQueryArgs, editDistance, expectElementDrift, nearestIsAmbiguous, nearestTestid, resolveQuery, testidDrift, } from './flow-anchor.js';
6
- import { routeOfEvent } from '@reticlehq/engine/question/predicate/predicate-route.js';
5
+ export { nearestIsAmbiguous, nearestTestid, resolveQuery } from './flow-anchor.js';
6
+ import { routeOfEvent, routeOfUrl } from '@reticlehq/engine/question/predicate/predicate-route.js';
7
7
  import { stepEffect } from '@reticlehq/engine/evidence/step-effect.js';
8
- import { AnchorKind, DriftReason, EventType, FlowStepTool, PredicateKind, } from '@reticlehq/core';
8
+ import { AnchorKind, DriftReason, EventType, FlowStepTool, StepEffect, PredicateKind, } from '@reticlehq/core';
9
9
  import { asString, isConsequenceDrift } from '@reticlehq/core';
10
10
  import { anchorFieldName } from './fields/flow-secret-field.js';
11
11
  import { degradedStepResult, isDegradedAnchor, runComponentStep, runRoleStep, runSequenceStep, } from './flow-step-runners.js';
@@ -48,8 +48,11 @@ const realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
48
48
  function currentRoute(session) {
49
49
  const routes = session.eventsSince(0).filter((e) => e.type === EventType.ROUTE_CHANGE);
50
50
  const last = routes.at(-1);
51
- if (last === undefined)
52
- return undefined;
51
+ // A page that loaded and never navigated has no route event; its URL is still where it is.
52
+ if (last === undefined) {
53
+ const loaded = session.url === undefined ? undefined : routeOfUrl(session.url);
54
+ return loaded !== undefined && loaded.routePath.length > 0 ? loaded.routePath : undefined;
55
+ }
53
56
  // The ROUTER's path. This field answers "which page did this step run on", and the document
54
57
  // pathname is `/` on every page of a hash-routed app — so a whole desktop replay reported `/` for
55
58
  // every step. Sixth place the same reading was wrong; see routeOfEvent.
@@ -280,6 +283,30 @@ async function runSignalStep(session, step, index, name, waitForSignal, signalTi
280
283
  },
281
284
  };
282
285
  }
286
+ /**
287
+ * The index a replay resumes at, or `undefined` when `from` names no step of this flow.
288
+ *
289
+ * A step declared `commits` in the prefix refuses the resume (0 = replay everything): re-driving it
290
+ * is not setup, it is a second charge, a second email, a second record.
291
+ */
292
+ export function resumeIndex(from, steps) {
293
+ if (from === undefined)
294
+ return 0;
295
+ const at = 'number' === typeof from ? from : steps.findIndex((step) => step.id === from);
296
+ if (at < 0 || at >= steps.length || !Number.isInteger(at))
297
+ return undefined;
298
+ return steps.slice(0, at).some((step) => StepEffect.COMMITS === step.effect) ? 0 : at;
299
+ }
300
+ /** The `from` argument, narrowed: a step index or a step id. */
301
+ export function resumeArg(raw) {
302
+ return 'number' === typeof raw || ('string' === typeof raw && raw.length > 0) ? raw : undefined;
303
+ }
304
+ /** Why `from` cannot be resumed at, or undefined when it can (or was not given). */
305
+ export function unresolvedResume(from, steps) {
306
+ if (resumeIndex(from, steps) !== undefined)
307
+ return undefined;
308
+ return `cannot resume at ${JSON.stringify(from)}: this flow has ${String(steps.length)} step(s) and no step by that id`;
309
+ }
283
310
  /**
284
311
  * Run an `invoke` step: load the named flow and replay it, or fail saying why.
285
312
  *
@@ -291,7 +318,7 @@ async function runSignalStep(session, step, index, name, waitForSignal, signalTi
291
318
  * own, and the sub-journey's outcome is summarised on the invocation, so a reader sees the shape
292
319
  * they recorded rather than a flattened list that has lost every boundary.
293
320
  */
294
- async function runInvokeStep(session, flow, step, index, waitForSignal, signalTimeoutMs, confirmDangerous, sleep, options) {
321
+ async function runInvokeStep(session, flow, step, index, waitForSignal, signalTimeoutMs, confirmDangerous, sleep, options, setup = false) {
295
322
  const name = step.invoke ?? '';
296
323
  const via = options.via ?? [];
297
324
  const here = { flow: flow.name, step: index, via };
@@ -313,7 +340,10 @@ async function runInvokeStep(session, flow, step, index, waitForSignal, signalTi
313
340
  error: `cannot replay "${name}": it was not found, so this journey would report green having never run it`,
314
341
  };
315
342
  }
316
- const carried = options;
343
+ // `from` is an offset into the CALLER's steps. A sub-journey invoked from the setup prefix runs
344
+ // whole and unchecked (from = its length); one invoked at or after the resume point runs normally.
345
+ const { from: _outer, ...rest } = options;
346
+ const carried = setup ? { ...rest, from: sub.steps.length } : rest;
317
347
  const nested = await replayFlow(session, sub, waitForSignal, signalTimeoutMs, confirmDangerous, sleep, {
318
348
  ...carried,
319
349
  via: [{ flow: flow.name, step: index }, ...via],
@@ -331,6 +361,9 @@ async function runInvokeStep(session, flow, step, index, waitForSignal, signalTi
331
361
  }
332
362
  export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs, confirmDangerous = false, sleep = realSleep, options = {}) {
333
363
  const results = [];
364
+ // Steps before this index are setup. `from` was validated by the caller; an unresolvable one
365
+ // resumes nowhere rather than everywhere, so it cannot silently widen into a full replay.
366
+ const from = resumeIndex(options.from, flow.steps) ?? flow.steps.length;
334
367
  // testids whose region is LLM-dynamic — their expect-presence is NOT asserted.
335
368
  const dynamic = new Set((flow.dynamic ?? [])
336
369
  .filter((a) => a.kind === AnchorKind.TESTID)
@@ -347,8 +380,10 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
347
380
  try {
348
381
  for (const step of flow.steps) {
349
382
  if (step.invoke !== undefined) {
350
- results.push(await runInvokeStep(session, flow, step, index, waitForSignal, signalTimeoutMs, confirmDangerous, sleep, options));
383
+ results.push(await runInvokeStep(session, flow, step, index, waitForSignal, signalTimeoutMs, confirmDangerous, sleep, options, index < from));
351
384
  const last = results[results.length - 1];
385
+ if (last !== undefined && index < from && last.ok)
386
+ results.pop();
352
387
  if (last !== undefined && false === last.ok)
353
388
  break;
354
389
  index += 1;
@@ -404,7 +439,9 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
404
439
  * replay path and belongs in its own commit.
405
440
  */
406
441
  const declared = [step.expect, ...(step.steps ?? []).map((sub) => sub.expect)].filter((expectation) => expectation !== undefined);
407
- for (const expectation of declared) {
442
+ // A setup step's consequence is not checked: it is re-driven to get somewhere, not to prove it.
443
+ const checked = index < from ? [] : declared;
444
+ for (const expectation of checked) {
408
445
  if (!result.ok || result.drift !== undefined)
409
446
  break;
410
447
  const expectDrift = await assertStepExpect(session, expectation, waitForSignal, waitFor(step), cursorBefore);
@@ -415,6 +452,9 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
415
452
  }
416
453
  if (page !== undefined)
417
454
  result.page = page;
455
+ const endPage = currentRoute(session);
456
+ if (endPage !== undefined && endPage !== page)
457
+ result.endPage = endPage;
418
458
  const windowEvents = session.eventsSince(cursorBefore).filter((e) => e.t >= cursorBefore);
419
459
  const consequence = summarizeConsequence(windowEvents);
420
460
  if (consequence !== undefined)
@@ -437,6 +477,12 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
437
477
  // forget the rule and quietly re-introduce the cost. Spelled out only when it is NOT the default.
438
478
  if (FlowStepTool.ACT === result.tool)
439
479
  delete result.tool;
480
+ if (index < from && result.ok && result.drift === undefined) {
481
+ index += 1;
482
+ continue;
483
+ }
484
+ if (index < from)
485
+ result.note = `setup step for resuming at step ${String(from)} failed`;
440
486
  results.push(result);
441
487
  // Under `sweep`, a failure whose action still RAN does not stop the run — the page is where the
442
488
  // step left it, so the next step is as meaningful as it was going to be. Anything else halts.
@@ -33,3 +33,5 @@ export type FlowResult<T> = {
33
33
  * hold the distinction — this is the fourth id joined into a path, and the one that file missed.
34
34
  */
35
35
  export declare const safeProjectId: (projectId?: ProjectId) => ProjectId | undefined;
36
+ /** Map a structured FlowErrorCode to a legible one-line message for the agent. */
37
+ export declare function flowErrorMessage(code: FlowErrorCode, detail?: string): string;
@@ -1,5 +1,6 @@
1
1
  import { FlowErrorCode } from '@reticlehq/core';
2
2
  import { SAFE_SEGMENT_PATTERN } from '@reticlehq/core';
3
+ import { FlowParseNote } from './flow-expect-grammar.js';
3
4
  /**
4
5
  * A project id that is safe to put in a path, or nothing.
5
6
  *
@@ -24,3 +25,27 @@ import { SAFE_SEGMENT_PATTERN } from '@reticlehq/core';
24
25
  * hold the distinction — this is the fourth id joined into a path, and the one that file missed.
25
26
  */
26
27
  export const safeProjectId = (projectId) => projectId !== undefined && SAFE_SEGMENT_PATTERN.test(projectId) ? projectId : undefined;
28
+ /** Map a structured FlowErrorCode to a legible one-line message for the agent. */
29
+ export function flowErrorMessage(code, detail) {
30
+ if (FlowErrorCode.PARSE_FAILED === code && undefined !== detail)
31
+ return detail;
32
+ // The detail names both versions and the remedy, so it beats anything generic this could say.
33
+ if (FlowErrorCode.WRONG_VERSION === code && undefined !== detail)
34
+ return detail;
35
+ switch (code) {
36
+ case FlowErrorCode.INVALID_NAME:
37
+ return 'invalid flow name — use a single safe segment (letters/digits/-/_), no path separators';
38
+ case FlowErrorCode.NOT_FOUND:
39
+ return 'no such flow on disk — run reticle_flow{action:"list"} to see saved flows';
40
+ case FlowErrorCode.PARSE_FAILED:
41
+ return FlowParseNote.MALFORMED;
42
+ case FlowErrorCode.NO_RECORDING:
43
+ return 'no compiled recording by that name — record one (reticle_record{action:"start"|"stop"}) first';
44
+ // Never "regenerate it": the file is intact and the reader is the wrong one. Telling somebody
45
+ // to rewrite an undamaged flow is the failure this code was split out of PARSE_FAILED to stop.
46
+ case FlowErrorCode.WRONG_VERSION:
47
+ return 'this flow file was written in a different flow-file format — the file is not damaged, this Reticle cannot read that version. Upgrade or downgrade Reticle rather than editing the flow';
48
+ case FlowErrorCode.STEP_NOT_FOUND:
49
+ return (detail ?? 'the step to resume at is not in this flow — pass a 0-based index or a step id');
50
+ }
51
+ }
@@ -1,3 +1,4 @@
1
+ import { flowAuthor } from './flow-author.js';
1
2
  import { FLOW_MUTATE_TOOL } from './flow-mutate-tools.js';
2
3
  import { seedSchema } from '../../surface/tools/args/numeric-bounds.js';
3
4
  import { z } from 'zod';
@@ -217,7 +218,12 @@ export const FLOW_TOOLS = [
217
218
  if (emptyRefusal !== undefined)
218
219
  return Promise.resolve(emptyRefusal);
219
220
  const { flows, root } = flowsForSession(deps, projectId);
220
- const toSave = saveAs === undefined ? program : { ...program, name: saveAs };
221
+ const author = flowAuthor();
222
+ const toSave = {
223
+ ...program,
224
+ ...(saveAs === undefined ? {} : { name: saveAs }),
225
+ ...(author === undefined ? {} : { author }),
226
+ };
221
227
  return flows.save(toSave, annotations, projectId).then(async (res) => {
222
228
  if (!res.ok)
223
229
  return { error: flowErrorMessage(res.code, res.detail), code: res.code };
@@ -382,6 +388,10 @@ export const FLOW_TOOLS = [
382
388
  .boolean()
383
389
  .optional()
384
390
  .describe('Set true to allow destructive controls during this replay only.'),
391
+ from: z
392
+ .union([z.number().int().nonnegative(), z.string().min(1)])
393
+ .optional()
394
+ .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.'),
385
395
  sweep: z
386
396
  .boolean()
387
397
  .optional()
@@ -636,9 +646,7 @@ export const FLOW_TOOLS = [
636
646
  return flow === undefined ? { replay } : { replay, flow };
637
647
  }));
638
648
  const timed = outcomes.map((o, i) => ({
639
- replay: o.ok && o.value !== undefined
640
- ? o.value.replay
641
- : leaseFailureReplay(requested[i] ?? '', o.error),
649
+ ...(parallelRuns[i] ?? { replay: leaseFailureReplay(requested[i] ?? '', o.error) }),
642
650
  durationMs: o.ok && o.value !== undefined ? o.value.durationMs : 0,
643
651
  }));
644
652
  const flaky = await recordSuiteFlakes(deps.fs, rootForProjectId(deps, projectId), parallelRuns);
@@ -671,7 +679,11 @@ export const FLOW_TOOLS = [
671
679
  .catch(() => null);
672
680
  const flow = loaded !== null && loaded.ok ? loaded.value : undefined;
673
681
  runs.push(flow === undefined ? { replay } : { replay, flow });
674
- timed.push({ replay, durationMs: deps.now() - start });
682
+ timed.push({
683
+ replay,
684
+ durationMs: deps.now() - start,
685
+ ...(flow === undefined ? {} : { flow }),
686
+ });
675
687
  }
676
688
  // The flake ledger the CLI gate already keeps — see FlakeStore. It was only ever written by
677
689
  // `reticle flow` on the command line, so an AGENT running this tool a hundred times learned
@@ -1,5 +1,5 @@
1
1
  import { type Predicate, type ProjectId } from '@reticlehq/core';
2
- export { REDACTED_FILL } from './fields/flow-secret-field.js';
2
+ export { REDACTED_FILL, secretEnvKey } from './fields/flow-secret-field.js';
3
3
  import { type FlowResult } from './flow-result.js';
4
4
  export type { FlowResult } from './flow-result.js';
5
5
  import type { FlowAnchor, FlowFile, FlowStep, HealChange, InstrumentationGap } from '@reticlehq/core';
@@ -65,6 +65,15 @@ export declare class FlowStore {
65
65
  * the flow before writing. Omitting `annotations` reproduces the same bytes.
66
66
  */
67
67
  save(program: CompiledProgram, annotations?: FlowAnnotations, projectId?: ProjectId): Promise<FlowResult<SaveSummary>>;
68
+ /**
69
+ * Save a flow the session drove, folded into any saved copy of the same journey.
70
+ *
71
+ * Every session saves its own drive, so one journey arrived as a near-copy per session. Flows
72
+ * under `prefix` (the auto-saved ones — a hand-named flow is never merged or removed) that start
73
+ * on the same page and take the same steps become ONE file: the oldest keeps its name, the others
74
+ * fill its gaps and are removed. The same scan collapses copies saved before this existed.
75
+ */
76
+ saveJourney(program: CompiledProgram, projectId: ProjectId | undefined, prefix: string): Promise<FlowResult<SaveSummary>>;
68
77
  /**
69
78
  * Persist an already-anchored FlowFile captured in-page (no recompilation). The
70
79
  * browser resolved every semantic anchor at capture time; here we only validate the name +