@reticlehq/server 3.2.0 → 3.3.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 (223) hide show
  1. package/SKILL.md +16 -13
  2. package/dist/command/cli/adhoc-suite.d.ts +19 -0
  3. package/dist/command/cli/adhoc-suite.js +88 -0
  4. package/dist/command/cli/adhoc-verdict.d.ts +23 -2
  5. package/dist/command/cli/adhoc-verdict.js +18 -10
  6. package/dist/command/cli/cli-doctor.js +25 -5
  7. package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
  8. package/dist/command/cli/cli-parse-grammar.js +7 -0
  9. package/dist/command/cli/cli-parse-verify.d.ts +2 -5
  10. package/dist/command/cli/cli-parse-verify.js +86 -13
  11. package/dist/command/cli/cli-parse.d.ts +7 -1
  12. package/dist/command/cli/cli-parse.js +33 -5
  13. package/dist/command/cli/cli-verify.d.ts +59 -0
  14. package/dist/command/cli/cli-verify.js +129 -17
  15. package/dist/command/cli/cloud-kit.d.ts +0 -2
  16. package/dist/command/cli/cloud-kit.js +1 -1
  17. package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
  18. package/dist/command/cli/launch/cli-launch.d.ts +1 -29
  19. package/dist/command/cli/launch/cli-launch.js +6 -40
  20. package/dist/command/cli/ports/resolve/cli-port.d.ts +13 -0
  21. package/dist/command/cli/ports/resolve/cli-port.js +45 -1
  22. package/dist/command/cli/report-command.d.ts +16 -0
  23. package/dist/command/cli/report-command.js +97 -0
  24. package/dist/command/cli.js +5 -0
  25. package/dist/command/daemon/binding/daemon-status-probe.d.ts +30 -0
  26. package/dist/command/daemon/binding/daemon-status-probe.js +45 -0
  27. package/dist/command/daemon/binding/port-presence.d.ts +1 -1
  28. package/dist/command/daemon/idle-shutdown.d.ts +2 -1
  29. package/dist/command/setup/init/init-host.js +8 -0
  30. package/dist/features/crawl/crawl.d.ts +2 -0
  31. package/dist/features/crawl/crawl.js +1 -0
  32. package/dist/features/harness/jev-driver.js +6 -9
  33. package/dist/features/harness/platform-config.d.ts +10 -0
  34. package/dist/features/harness/platform-config.js +8 -0
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.js +40 -95
  37. package/dist/judgement/capsule/capsule-store.d.ts +31 -2
  38. package/dist/judgement/capsule/capsule-store.js +58 -6
  39. package/dist/judgement/capsule/capsule.js +5 -0
  40. package/dist/judgement/capsule/causal-summary.d.ts +13 -1
  41. package/dist/judgement/capsule/causal-summary.js +7 -0
  42. package/dist/judgement/domain/domain-model.js +38 -9
  43. package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
  44. package/dist/judgement/phenomena/phenomena.d.ts +2 -1
  45. package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
  46. package/dist/judgement/runs/artifact/gap-report.js +46 -0
  47. package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
  48. package/dist/judgement/runs/artifact/gap-summary.js +85 -0
  49. package/dist/judgement/runs/artifact/render-report.js +4 -1
  50. package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
  51. package/dist/judgement/runs/artifact/to-artifact.js +1 -1
  52. package/dist/judgement/runs/context-tools.js +21 -3
  53. package/dist/judgement/runs/drive-run.js +7 -2
  54. package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
  55. package/dist/language/flows/annotate-notes/annotate.js +66 -18
  56. package/dist/language/flows/flow-anchor.d.ts +12 -0
  57. package/dist/language/flows/flow-anchor.js +20 -0
  58. package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
  59. package/dist/language/flows/flow-expect-grammar.js +92 -11
  60. package/dist/language/flows/flow-intent.js +1 -1
  61. package/dist/language/flows/flow-learning.d.ts +3 -0
  62. package/dist/language/flows/flow-learning.js +39 -8
  63. package/dist/language/flows/flow-replay-run.d.ts +31 -9
  64. package/dist/language/flows/flow-replay-run.js +146 -33
  65. package/dist/language/flows/flow-replay-types.d.ts +9 -0
  66. package/dist/language/flows/flow-replay.d.ts +12 -11
  67. package/dist/language/flows/flow-replay.js +79 -109
  68. package/dist/language/flows/flow-step-runners.d.ts +26 -10
  69. package/dist/language/flows/flow-step-runners.js +20 -4
  70. package/dist/language/flows/flow-success.d.ts +21 -6
  71. package/dist/language/flows/flow-success.js +50 -138
  72. package/dist/language/flows/flows.d.ts +8 -4
  73. package/dist/language/flows/flows.js +11 -2
  74. package/dist/language/flows/heal.d.ts +9 -1
  75. package/dist/language/flows/heal.js +24 -2
  76. package/dist/language/flows/learned-sources.d.ts +21 -0
  77. package/dist/language/flows/learned-sources.js +84 -0
  78. package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
  79. package/dist/language/flows/recording/recording-backtrack.js +1 -1
  80. package/dist/language/flows/recording/replay-halt.d.ts +2 -1
  81. package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
  82. package/dist/language/flows/replay.js +12 -8
  83. package/dist/language/flows/stores/annotation-store.d.ts +5 -5
  84. package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
  85. package/dist/language/flows/stores/flake-store.js +2 -3
  86. package/dist/language/flows/suite/coverage.d.ts +1 -1
  87. package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
  88. package/dist/language/flows/verify-change-tools.js +14 -0
  89. package/dist/memory/cloud/cloud-sync.d.ts +0 -2
  90. package/dist/memory/cloud/cloud-sync.js +1 -1
  91. package/dist/memory/cloud/harness-config.d.ts +2 -0
  92. package/dist/memory/cloud/sync-cycle.d.ts +12 -0
  93. package/dist/memory/cloud/sync-cycle.js +29 -4
  94. package/dist/memory/cloud/sync-daemon.d.ts +13 -0
  95. package/dist/memory/cloud/sync-daemon.js +21 -0
  96. package/dist/memory/cloud/sync-disk.js +11 -6
  97. package/dist/memory/impact/impact-store.js +6 -0
  98. package/dist/memory/intent/inline-intent.js +8 -3
  99. package/dist/memory/intent/intent-shard-store.d.ts +33 -47
  100. package/dist/memory/intent/intent-shard-store.js +252 -140
  101. package/dist/memory/intent/intent-shard.d.ts +10 -11
  102. package/dist/memory/intent/intent-shard.js +1 -1
  103. package/dist/memory/intent/intent-store.d.ts +13 -8
  104. package/dist/memory/intent/intent-store.js +46 -90
  105. package/dist/memory/intent/intent-tools.js +34 -23
  106. package/dist/memory/journal/ambient-store.js +2 -3
  107. package/dist/memory/journal/deviation-report.js +4 -1
  108. package/dist/memory/journal/deviation-service.js +10 -2
  109. package/dist/memory/journal/drive-flow.d.ts +2 -2
  110. package/dist/memory/journal/drive-flow.js +21 -12
  111. package/dist/memory/journal/envelope-store.js +2 -3
  112. package/dist/memory/journal/journal-recorder.d.ts +21 -0
  113. package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
  114. package/dist/memory/journal/on-disk/retain-policy.js +44 -0
  115. package/dist/memory/journal/on-disk/retention.d.ts +14 -10
  116. package/dist/memory/journal/on-disk/retention.js +13 -12
  117. package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
  118. package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
  119. package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -11
  120. package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -45
  121. package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
  122. package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
  123. package/dist/memory/journal/route-template.d.ts +22 -0
  124. package/dist/memory/journal/route-template.js +44 -0
  125. package/dist/memory/journal/session-end.d.ts +4 -1
  126. package/dist/memory/journal/session-end.js +64 -6
  127. package/dist/memory/journal/session-journal.d.ts +8 -1
  128. package/dist/memory/journal/session-journal.js +10 -0
  129. package/dist/memory/project/baselines.d.ts +1 -1
  130. package/dist/memory/project/dir/reticle-dir.d.ts +1 -1
  131. package/dist/memory/project/fs/fs-port.js +44 -5
  132. package/dist/memory/project/fs/write-atomic.d.ts +26 -0
  133. package/dist/memory/project/fs/write-atomic.js +48 -0
  134. package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
  135. package/dist/memory/recall/prior/connection-memory.js +8 -0
  136. package/dist/memory/recall/registered-projects.d.ts +2 -1
  137. package/dist/portal/bridge/auth-failure-reason.js +5 -2
  138. package/dist/portal/input/real-input.js +2 -1
  139. package/dist/portal/pool/browser-pool.d.ts +10 -0
  140. package/dist/portal/pool/browser-pool.js +10 -0
  141. package/dist/portal/pool/playwright-launcher.js +1 -0
  142. package/dist/portal/pool/pool-contract.d.ts +2 -0
  143. package/dist/portal/pool/storage-seed.d.ts +2 -1
  144. package/dist/portal/pool/zero-install.d.ts +27 -0
  145. package/dist/portal/pool/zero-install.js +54 -0
  146. package/dist/portal/realm/web-realm.js +6 -0
  147. package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
  148. package/dist/portal/session/dev-server/served-document.d.ts +21 -0
  149. package/dist/portal/session/dev-server/served-document.js +57 -0
  150. package/dist/portal/session/facts/background-traffic.d.ts +9 -0
  151. package/dist/portal/session/facts/background-traffic.js +25 -0
  152. package/dist/portal/session/live-control-tools.js +48 -3
  153. package/dist/portal/session/presence/attachment-history.d.ts +13 -2
  154. package/dist/portal/session/session-health.d.ts +61 -0
  155. package/dist/portal/session/session-health.js +40 -1
  156. package/dist/portal/session/session-info.d.ts +6 -0
  157. package/dist/portal/session/session-manager.d.ts +9 -0
  158. package/dist/portal/session/session-manager.js +23 -2
  159. package/dist/portal/session/session-successor.d.ts +11 -0
  160. package/dist/portal/session/session-successor.js +14 -1
  161. package/dist/portal/session/session-verdict-facts.d.ts +28 -0
  162. package/dist/portal/session/session-verdict-facts.js +15 -0
  163. package/dist/portal/session/session.d.ts +5 -0
  164. package/dist/portal/session/session.js +19 -7
  165. package/dist/surface/mcp/mcp-proxy.js +3 -3
  166. package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
  167. package/dist/surface/mcp/server-instructions.js +12 -1
  168. package/dist/surface/tools/act/already-true.d.ts +40 -0
  169. package/dist/surface/tools/act/already-true.js +36 -0
  170. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
  171. package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
  172. package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
  173. package/dist/surface/tools/act/settle-in-flight.js +29 -6
  174. package/dist/surface/tools/act-capsule.js +22 -7
  175. package/dist/surface/tools/act-tools.js +25 -24
  176. package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
  177. package/dist/surface/tools/args/numeric-bounds.js +30 -3
  178. package/dist/surface/tools/assert/assert-grade.js +14 -2
  179. package/dist/surface/tools/assert/assert-verdict.js +30 -4
  180. package/dist/surface/tools/browser-tools.js +4 -3
  181. package/dist/surface/tools/dynamic-tools.js +6 -0
  182. package/dist/surface/tools/feedback-tools.d.ts +2 -1
  183. package/dist/surface/tools/feedback-tools.js +1 -1
  184. package/dist/surface/tools/invoke-tool.js +17 -4
  185. package/dist/surface/tools/lease-hint.d.ts +8 -0
  186. package/dist/surface/tools/lease-hint.js +10 -0
  187. package/dist/surface/tools/lease-tools.d.ts +16 -28
  188. package/dist/surface/tools/lease-tools.js +53 -5
  189. package/dist/surface/tools/lineage-tools.js +1 -1
  190. package/dist/surface/tools/observe-tools.js +14 -6
  191. package/dist/surface/tools/real-input-attempt.d.ts +0 -5
  192. package/dist/surface/tools/real-input-attempt.js +1 -1
  193. package/dist/surface/tools/tool-kit.d.ts +5 -0
  194. package/dist/surface/tools/tool-kit.js +6 -1
  195. package/dist/surface/tools/tool-surface.js +17 -5
  196. package/dist/surface/tools/tools.js +64 -29
  197. package/dist/telemetry/feedback-local.d.ts +1 -0
  198. package/dist/telemetry/feedback-local.js +1 -3
  199. package/dist/telemetry/feedback.d.ts +1 -1
  200. package/dist/telemetry/hud-metrics.d.ts +32 -0
  201. package/dist/telemetry/hud-metrics.js +95 -0
  202. package/dist/telemetry/session-metrics.d.ts +5 -0
  203. package/dist/telemetry/session-metrics.js +15 -1
  204. package/dist/wire-journal.d.ts +31 -0
  205. package/dist/wire-journal.js +100 -0
  206. package/docs/README.md +1 -0
  207. package/docs/cli/cloud.mdx +9 -7
  208. package/docs/cli/init.mdx +1 -0
  209. package/docs/cli/report.mdx +44 -0
  210. package/docs/desktop.mdx +1 -1
  211. package/docs/flows.md +11 -0
  212. package/docs/frameworks.mdx +113 -1
  213. package/docs/install-agentic.mdx +36 -0
  214. package/docs/install-manual.mdx +1 -1
  215. package/docs/packages/tauri.mdx +1 -1
  216. package/docs/packages.mdx +1 -1
  217. package/docs/predicates.mdx +128 -4
  218. package/docs/tools/query.mdx +10 -10
  219. package/docs/usage.md +1 -1
  220. package/docs/what-is-recorded.md +62 -0
  221. package/package.json +6 -6
  222. package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
  223. package/dist/judgement/outcome/predicate-to-expect.js +0 -148
@@ -6,7 +6,7 @@ import { asRecord, asString } from '@reticlehq/core';
6
6
  import { routeOfEvent, routeOfUrl } from '@reticlehq/engine/question/predicate/predicate-route.js';
7
7
  import { carryReticleIdentity } from '../../surface/tools/lease-tools.js';
8
8
  import { replayFlow } from './flow-replay.js';
9
- import { anchorQueryArgs } from './flow-step-runners.js';
9
+ import { anchorPrecondition, anchorQueryArgs } from './flow-step-runners.js';
10
10
  import { queryRefs } from './replay.js';
11
11
  import { assertSuccess, dynamicTestids, successLabel, SUCCESS_STEP_TOOL } from './flow-success.js';
12
12
  import { buildDecision, unverifiableReason } from './decision.js';
@@ -47,6 +47,9 @@ export function latestRecordedFlow(events) {
47
47
  export function flowErrorMessage(code, detail) {
48
48
  if (FlowErrorCode.PARSE_FAILED === code && undefined !== detail)
49
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;
50
53
  switch (code) {
51
54
  case FlowErrorCode.INVALID_NAME:
52
55
  return 'invalid flow name — use a single safe segment (letters/digits/-/_), no path separators';
@@ -56,6 +59,10 @@ export function flowErrorMessage(code, detail) {
56
59
  return FlowParseNote.MALFORMED;
57
60
  case FlowErrorCode.NO_RECORDING:
58
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';
59
66
  }
60
67
  }
61
68
  /** Map the wire ReplayStatus onto the persisted RunStatus (ok→pass). */
@@ -106,20 +113,46 @@ recordRoot) {
106
113
  function currentPathOf(session) {
107
114
  const routes = session.eventsSince(0).filter((e) => e.type === EventType.ROUTE_CHANGE);
108
115
  const last = routes.at(-1);
109
- // pathname + hash, because `startPath` is compared against this and must stay NAVIGABLE. Reading
110
- // the pathname alone made both sides `/` on a hash router — always "same path", so the hint never
111
- // fired however far the tab had drifted, on the router desktop renderers use by default.
116
+ // pathname + search + hash, because `startPath` is compared against this and must stay NAVIGABLE.
117
+ //
118
+ // Reading the pathname alone made both sides `/` on a hash router — always "same path", so the
119
+ // hint never fired however far the tab had drifted, on the router desktop renderers use by
120
+ // default. Leaving the SEARCH out was the mirror defect: `startPath` keeps its query, so a flow
121
+ // starting at `/admin/events/7?tab=wrap` never matched a tab sitting on exactly that, re-navigated
122
+ // on every replay, and the re-navigation killed the session mid-flow.
123
+ //
124
+ // Comparing without the query instead would have been worse in the direction that matters. The
125
+ // query usually decides what the page renders, so a tab on `?tab=summary` would read as "already
126
+ // at `?tab=wrap`" and the replay would start on the wrong page with nothing saying so. Both sides
127
+ // carry it, which keeps a real difference visible and stops the false one.
112
128
  const observed = last === undefined ? undefined : routeOfEvent(last);
113
129
  if (observed !== undefined)
114
- return `${observed.docPath}${observed.hash}`;
130
+ return `${observed.docPath}${observed.search}${observed.hash}`;
115
131
  if (session.url === undefined)
116
132
  return undefined;
117
133
  const fromUrl = routeOfUrl(session.url);
118
- return fromUrl === undefined ? undefined : `${fromUrl.docPath}${fromUrl.hash}`;
134
+ return fromUrl === undefined ? undefined : `${fromUrl.docPath}${fromUrl.search}${fromUrl.hash}`;
119
135
  }
120
- /** Pathname equality up to a trailing slash — a router normalising one must not read as "elsewhere". */
121
- function samePath(a, b) {
122
- return a.replace(/\/$/, '') === b.replace(/\/$/, '');
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);
123
156
  }
124
157
  /**
125
158
  * Ask the project what it already knows about this flow.
@@ -201,9 +234,26 @@ const REAL_ARRIVAL_CLOCK = {
201
234
  * `resolve` throws). Keyed to the navigated tab's own identity on purpose: matching "any session at
202
235
  * the target page" would happily hand replay an unrelated tab that was already sitting there.
203
236
  */
204
- function arrivedSuccessor(sessions, oldId, target) {
237
+ function arrivedSuccessor(sessions, oldId, target,
238
+ /** The handle that issued the navigation, when only a genuinely new one counts as arrival. */
239
+ mustReplace) {
205
240
  try {
206
241
  const candidate = sessions.resolve(oldId);
242
+ /*
243
+ * A RELOAD lands on the path it left, so the path cannot tell the successor from the tab that
244
+ * is still tearing down — `resolve(oldId)` answers with the dying one while it is registered,
245
+ * which looks like instant arrival and hands replay a socket that will never answer again.
246
+ *
247
+ * The discriminator is the OBJECT, not the id. A leased tab carries its identity across a page
248
+ * load (that is what the identity params are for), so it reconnects under the SAME id — waiting
249
+ * for a new id there waits forever, and the first lease this was tried on timed out at step 1
250
+ * on a tab that was perfectly healthy. `SessionManager.add` replaces the instance on every
251
+ * reconnect, so a different instance is exactly "the socket came back".
252
+ *
253
+ * A navigation to a different route has the path as its discriminator and does not need this.
254
+ */
255
+ if (mustReplace !== undefined && candidate === mustReplace)
256
+ return undefined;
207
257
  const path = currentPathOf(candidate);
208
258
  return path !== undefined && samePath(path, target) ? candidate : undefined;
209
259
  }
@@ -242,36 +292,83 @@ async function firstStepResolvesHere(session, flow) {
242
292
  }
243
293
  }
244
294
  /**
245
- * Replay's half of the FlowFile contract: navigate to the flow's `startPath` before step 1.
295
+ * Replay's half of the FlowFile contract: put the tab on the flow's `startPath` before step 1, and
296
+ * RELOAD it if it is already there.
297
+ *
298
+ * The reload is the state contract. Replaying "add a task" twice against a tab nobody reset gives
299
+ * the second run a world the recording never saw, and its assertions then describe the first run
300
+ * rather than the app. Our own `replay-determinism.mjs` has always refreshed before every run — the
301
+ * determinism we measured was never the determinism a user got.
302
+ *
303
+ * `requires` is the declared opt-out, and its first caller. A flow that says it starts from state
304
+ * another flow established is the one flow a page load would destroy, so it is never reset — here
305
+ * or from the wrong page. Silence is not an opt-out: a flow that depends on leftovers without
306
+ * saying so is a flow whose green means nothing on a fresh machine, and CI is a fresh machine.
246
307
  *
247
308
  * A full-page load tears down the session socket, so the navigation must happen here — before any
248
309
  * step runs — and the replay continues on the session the SDK reconnects as (found via the same
249
310
  * tombstone rebind that lets `resolve(oldId)` answer after any navigation). Best-effort by design:
250
- * when the flow carries no startPath, the tab is already there, the current route is unobservable,
251
- * the navigation is refused, or the SDK never reconnects in the window, it returns undefined and
252
- * replay proceeds on the connected session as before — with startPathMismatchHint turning any
253
- * resulting drift into an actionable next move rather than a mystifying one.
311
+ * when the flow carries no startPath, the current route is unobservable, the navigation is refused,
312
+ * or the SDK never reconnects in the window, replay proceeds on the connected session as before —
313
+ * with startPathMismatchHint turning any resulting drift into an actionable next move rather than a
314
+ * mystifying one.
254
315
  */
255
316
  export async function arriveAtStartPath(sessions, session, flow, timeoutMs = START_PATH_ARRIVAL_TIMEOUT_MS, clock = REAL_ARRIVAL_CLOCK) {
256
317
  const target = flow.startPath;
257
318
  if (target === undefined || 0 === target.length)
258
- return undefined;
319
+ return {};
259
320
  const current = currentPathOf(session);
260
- if (current === undefined || samePath(current, target))
261
- return undefined;
262
- // The route differs — but that only matters if it stops the flow starting. See above.
263
- if (await firstStepResolvesHere(session, flow))
264
- return undefined;
321
+ if (current === undefined)
322
+ return {};
323
+ // The declared opt-out, both directions. See above.
324
+ if (0 < (flow.requires?.length ?? 0))
325
+ return {};
326
+ const here = samePath(current, target);
327
+ // A route mismatch that step 1 can start from anyway is not worth a page load: navigating away
328
+ // from a persistent anchor could only hurt, and did. Arriving is the goal; resetting a tab that is
329
+ // ALREADY here is a different decision, taken above, and this must not override it.
330
+ if (!here && (await firstStepResolvesHere(session, flow)))
331
+ return {};
265
332
  let destination;
266
333
  try {
267
334
  // startPath is a pathname (a host belongs to the machine, not the journey) — resolve it
268
- // against the tab's own URL to get something the browser can be sent to.
269
- destination = new URL(target, session.url).toString();
335
+ // against the tab's own URL to get something the browser can be sent to. Already HERE, the
336
+ // reset reloads the page the tab is on: `samePath` ignored a query the flow did not record, and
337
+ // navigating to `target` would strip it, so the replay would test a different page than the one
338
+ // it just agreed it was on.
339
+ destination = new URL(here ? current : target, session.url).toString();
270
340
  }
271
341
  catch {
272
- return undefined; // no usable base URL — nowhere to navigate from
342
+ return {}; // no usable base URL — nowhere to navigate from
273
343
  }
274
- return navigateAndAwait(sessions, session, destination, target, timeoutMs, clock);
344
+ // Measured either side of the reset, and only when the reset is a reload: reachable before and
345
+ // gone after is the reload's doing, not the flow file's. Without this, an app holding its session
346
+ // in memory comes back signed out and step 1 blames a component that is completely fine.
347
+ const resolvedBefore = here && (await firstStepResolvesHere(session, flow));
348
+ const arrived = await navigateAndAwait(sessions, session, destination, target, timeoutMs, clock, here);
349
+ if (arrived === undefined)
350
+ return {};
351
+ if (!resolvedBefore || (await firstStepResolvesHere(arrived, flow)))
352
+ return { session: arrived };
353
+ return {
354
+ session: arrived,
355
+ resetCost: resetCostHint(flow, target),
356
+ };
357
+ }
358
+ /**
359
+ * Why step 1 failed after the reset, and the one line that opts the flow out.
360
+ *
361
+ * No tool writes `requires`, so naming the field was advice nobody could act on without guessing its
362
+ * shape. The first anchor IS the precondition: the element step 1 needs, present before it runs.
363
+ */
364
+ function resetCostHint(flow, target) {
365
+ const first = flow.steps?.[0];
366
+ const needs = first === undefined ? undefined : anchorPrecondition(first.anchor);
367
+ const optOut = needs === undefined
368
+ ? 'Declare what it needs (`requires`) in the flow file to opt out of the reset'
369
+ : `Add \`"requires":${JSON.stringify([needs])}\` to the flow file to opt out of the reset`;
370
+ return (`replay reloaded ${target} before step 1 and the first anchor did not come back — this flow ` +
371
+ `starts from state a page load discards. ${optOut}, or record it from a cold page`);
275
372
  }
276
373
  /**
277
374
  * Send a tab somewhere and wait for the session that comes back.
@@ -285,7 +382,9 @@ export async function arriveAtStartPath(sessions, session, flow, timeoutMs = STA
285
382
  * to move and this decides how, which is also why the deliberate short-circuits live up there and
286
383
  * not in here: a caller that has already decided it must move should not have to argue with them.
287
384
  */
288
- export async function navigateAndAwait(sessions, session, destination, expectedPath, timeoutMs = START_PATH_ARRIVAL_TIMEOUT_MS, clock = REAL_ARRIVAL_CLOCK) {
385
+ export async function navigateAndAwait(sessions, session, destination, expectedPath, timeoutMs = START_PATH_ARRIVAL_TIMEOUT_MS, clock = REAL_ARRIVAL_CLOCK,
386
+ /** True when the destination is the page we are already on — see `arrivedSuccessor`. */
387
+ isReload = false) {
289
388
  // A leased tab is addressed by URL params, so navigating without them would strand the lease.
290
389
  const url = carryReticleIdentity(session.url, destination);
291
390
  try {
@@ -303,7 +402,7 @@ export async function navigateAndAwait(sessions, session, destination, expectedP
303
402
  }
304
403
  const deadline = clock.now() + timeoutMs;
305
404
  for (;;) {
306
- const arrived = arrivedSuccessor(sessions, session.id, expectedPath);
405
+ const arrived = arrivedSuccessor(sessions, session.id, expectedPath, isReload ? session : undefined);
307
406
  if (arrived !== undefined)
308
407
  return arrived;
309
408
  if (clock.now() >= deadline)
@@ -361,7 +460,15 @@ async function firstUnmetPrecondition(session, flow, since) {
361
460
  for (const claim of flow.requires ?? []) {
362
461
  // Zero budget: a precondition is a claim about the state you are starting FROM. Waiting for one
363
462
  // turns "was it true" into "did it become true", which is a different and much weaker question.
364
- const drift = await assertStepExpect(session, claim, new Set(), waitForPredicate, 0, since);
463
+ let drift;
464
+ try {
465
+ drift = await assertStepExpect(session, claim, waitForPredicate, 0, since);
466
+ }
467
+ catch (error) {
468
+ if (!isDocumentGoneError(error))
469
+ throw error;
470
+ return `the page went away while this flow's preconditions were being checked, so nothing ran and nothing was proved. Replay again once the page is back.`;
471
+ }
365
472
  if (drift !== undefined) {
366
473
  return `a precondition of this flow does not hold (${JSON.stringify(claim)}), so nothing ran and nothing was proved. Run the flow that establishes it first, or drive that state yourself.`;
367
474
  }
@@ -390,7 +497,8 @@ export function lostDocumentResult(name, lost) {
390
497
  status: ReplayStatus.OK,
391
498
  steps: lost.steps,
392
499
  unverifiable: {
393
- reason: `the page loaded a new document while step ${String(lost.atStep)} was running, so the run ` +
500
+ reason: `the page's connection to Reticle was replaced while step ${String(lost.atStep)} was running ` +
501
+ `(a new document loaded, or its socket reconnected), so the run ` +
394
502
  `could not be graded — the steps before it are reported and nothing after it was observed. ` +
395
503
  `Nothing here says the app failed. If that navigation is part of the journey, record the ` +
396
504
  `step's consequence with \`expect\` and replay again; if it was not, the flow started ` +
@@ -398,7 +506,9 @@ export function lostDocumentResult(name, lost) {
398
506
  },
399
507
  };
400
508
  }
401
- export async function replayNamedFlow(deps, args) {
509
+ export async function replayNamedFlow(deps, args,
510
+ /** Wraps the session the steps run on, so a caller can watch what they resolved. */
511
+ observe = (s) => s) {
402
512
  const startedAt = deps.now();
403
513
  const name = asString(args['flowName']) ?? '';
404
514
  // Resolve within the connecting app's scope so a shared daemon replays THIS project's flow, not a
@@ -438,8 +548,11 @@ export async function replayNamedFlow(deps, args) {
438
548
  // The FlowFile contract: replay navigates to the flow's startPath before step 1, and the steps run
439
549
  // on the session the SDK reconnects as. When arrival can't be confirmed, replay proceeds on the
440
550
  // connected session — and the hint below turns the wrong-page drift into an actionable next move.
441
- const session = (await arriveAtStartPath(deps.sessions, connected, loaded.value)) ?? connected;
442
- const startPathHint = startPathMismatchHint(loaded.value, session);
551
+ const arrival = await arriveAtStartPath(deps.sessions, connected, loaded.value);
552
+ const session = arrival.session ?? connected;
553
+ // The reset's own cost outranks the wrong-page hint: if the reload is why step 1 cannot start,
554
+ // "navigate there and replay" is advice that would do the same thing again.
555
+ const startPathHint = arrival.resetCost ?? startPathMismatchHint(loaded.value, session);
443
556
  // Floor the success oracle at the start of THIS replay so a stale signal from a prior run
444
557
  // in the same session can't fake a pass.
445
558
  const replayFloor = session.elapsed();
@@ -506,7 +619,7 @@ export async function replayNamedFlow(deps, args) {
506
619
  }
507
620
  let steps;
508
621
  try {
509
- steps = await replayFlow(session, replayable, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, true === args['confirmDangerous'], undefined, {
622
+ steps = await replayFlow(observe(session), replayable, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, true === args['confirmDangerous'], undefined, {
510
623
  // How an `invoke` step finds the flow it runs. Scoped to the same project as the flow being
511
624
  // replayed, so a composite cannot reach into another app's store for a same-named sub-journey.
512
625
  resolveFlow: async (invoked) => {
@@ -26,6 +26,15 @@ export interface FlowReplaySession {
26
26
  onEvent(listener: (event: ReticleEvent) => void): () => void;
27
27
  /** Buffer clock (ms since connect) — required by the predicate engine's `settled` check. */
28
28
  elapsed(): number;
29
+ /**
30
+ * Where the app under test lives, so a third-party beacon is not mistaken for the app still
31
+ * working. Optional for the same reason `beginAction` is — a minimal test double should not have
32
+ * to invent an origin — and it FAILS OPEN: with no origin nothing is foreign, so an omitted value
33
+ * suppresses nothing rather than silently widening what a replay ignores.
34
+ */
35
+ url?: string;
36
+ /** Same-origin endpoints the project declared as background — see `Session.background`. */
37
+ background?: readonly string[];
29
38
  }
30
39
  /**
31
40
  * The injected predicate-waiter (the real waitForPredicate) — reused, never reimplemented.
@@ -1,4 +1,3 @@
1
- import { Surface } from 'open-verification';
2
1
  export { anchorLabel, componentLabel, componentQueryArgs, editDistance, expectElementDrift, nearestIsAmbiguous, nearestTestid, resolveQuery, testidDrift, } from './flow-anchor.js';
3
2
  import type { FlowReplaySession, WaitForSignal, Sleep } from './flow-replay-types.js';
4
3
  export type { FlowReplaySession, WaitForSignal, Sleep } from './flow-replay-types.js';
@@ -20,6 +19,8 @@ export declare class DocumentLostDuringReplay extends Error {
20
19
  readonly atStep: number;
21
20
  constructor(steps: FlowStepResult[], atStep: number, lostTo?: unknown);
22
21
  }
22
+ /** Run one testid-anchored step: re-resolve via QUERY, then ACT on the live ref, else drift. */
23
+ export declare function runTestidStep(session: FlowReplaySession, step: FlowStep, index: number, value: string, dynamic: ReadonlySet<string>, confirmDangerous: boolean, sleep: Sleep): Promise<FlowStepResult>;
23
24
  /**
24
25
  * After a step's anchor resolves and its action runs, its `expect` is EVALUATED — every kind of it.
25
26
  *
@@ -41,16 +42,23 @@ export declare class DocumentLostDuringReplay extends Error {
41
42
  * quietly stop agreeing, and the disagreement would land as a flow that replays green against a
42
43
  * state it was never meant to run in.
43
44
  */
44
- export declare function assertStepExpect(session: FlowReplaySession, expect: NonNullable<FlowStep['expect']>, dynamic: ReadonlySet<string>, waitForSignal: WaitForSignal, timeoutMs: number, since: number): Promise<Drift | undefined>;
45
+ export declare function assertStepExpect(session: FlowReplaySession, expect: NonNullable<FlowStep['expect']>, waitForSignal: WaitForSignal, timeoutMs: number, since: number): Promise<Drift | undefined>;
45
46
  /**
46
47
  * Replay a loaded flow by RE-RESOLVING every step's semantic anchor against the live DOM — never
47
48
  * a stale ref. A testid anchor is re-found by reticle_query; a signal anchor waits on a predicate.
48
49
  * On the first anchor MISS the step carries legible drift and replay STOPS, returning the partial
49
50
  * results. This is the "whose fault is it" contract, not a blind "command failed".
50
51
  */
51
- /** Where a replay starts reporting. Steps before it are re-driven, silently, as setup. */
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.
60
+ */
52
61
  export interface ReplayFromOptions {
53
- from?: number;
54
62
  /**
55
63
  * How to load a flow this one INVOKES. Absent means invocations cannot be followed.
56
64
  *
@@ -67,13 +75,6 @@ export interface ReplayFromOptions {
67
75
  flow: string;
68
76
  step: number;
69
77
  }[];
70
- /**
71
- * Which kind of subject this is. Decides whether re-driving the prefix is allowed at all.
72
- *
73
- * Defaults to `web`, which is the permissive answer — correct for the only surface that resumes
74
- * today, and the reason a caller on a committing surface must say so rather than rely on silence.
75
- */
76
- surface?: Surface;
77
78
  /**
78
79
  * Keep going past a step whose ACTION ran and whose declared consequence merely did not hold.
79
80
  *