@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
@@ -1,23 +1,21 @@
1
- import { mayResumeByReplayingPrefix, StepEffect } from '@reticlehq/core';
2
- import { Surface, formatStepAddress } from 'open-verification';
1
+ import { formatStepAddress } from 'open-verification';
2
+ import { assertsState, expectLabel, expectedElementTestid, withoutClauses } from '@reticlehq/core';
3
3
  import { span } from '../../trace.js';
4
- import { anchorLabel, expectElementDrift, resolveTestid, testidDrift } from './flow-anchor.js';
4
+ import { anchorLabel, expectElementDrift, resolveTestid, testidDrift, ambiguousAnchorDrift, } from './flow-anchor.js';
5
5
  export { anchorLabel, componentLabel, componentQueryArgs, editDistance, expectElementDrift, nearestIsAmbiguous, nearestTestid, resolveQuery, testidDrift, } from './flow-anchor.js';
6
6
  import { routeOfEvent } 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, ReticleCommand, PredicateKind, } from '@reticlehq/core';
8
+ import { AnchorKind, DriftReason, EventType, FlowStepTool, PredicateKind, } from '@reticlehq/core';
9
9
  import { asString, isConsequenceDrift } from '@reticlehq/core';
10
- import { replayActionArgs, ambiguousTestidNote } from './replay.js';
11
10
  import { anchorFieldName } from './fields/flow-secret-field.js';
12
11
  import { degradedStepResult, isDegradedAnchor, runComponentStep, runRoleStep, runSequenceStep, } from './flow-step-runners.js';
13
- import { successToPredicate } from './flow-success.js';
14
12
  import { inFlightRequestLabels } from '../../surface/tools/act/settle-in-flight.js';
15
13
  import { namedNetIsInFlight } from '@reticlehq/engine/evidence/unsettled.js';
16
14
  /** What a still-open named request means, in the drift's own words. */
17
15
  const IN_FLIGHT_AT_BUDGET_END = 'the request this step declared had not come back when the budget ended — it is still in flight, ' +
18
16
  'so nothing here says the app failed. Raise the step timeout, or look at the endpoint';
19
- import { ReticleTool } from '@reticlehq/core';
20
- import { isDocumentGoneError } from '../../portal/session/facts/session-replaced.js';
17
+ import { isDocumentGoneError, SESSION_DISCONNECTED_REASON, } from '../../portal/session/facts/session-replaced.js';
18
+ import { actOnResolvedRef } from './flow-step-runners.js';
21
19
  /**
22
20
  * The document this replay was driving went away mid-run.
23
21
  *
@@ -105,7 +103,7 @@ function summarizeConsequence(events) {
105
103
  return parts.length > 0 ? parts.join('; ') : undefined;
106
104
  }
107
105
  /** Run one testid-anchored step: re-resolve via QUERY, then ACT on the live ref, else drift. */
108
- async function runTestidStep(session, step, index, value, dynamic, confirmDangerous, sleep) {
106
+ export async function runTestidStep(session, step, index, value, dynamic, confirmDangerous, sleep) {
109
107
  const { refs, hint } = await resolveTestid(session, value, sleep);
110
108
  if (0 === refs.length) {
111
109
  return {
@@ -116,35 +114,50 @@ async function runTestidStep(session, step, index, value, dynamic, confirmDanger
116
114
  drift: testidDrift(value, hint),
117
115
  };
118
116
  }
119
- const ref = refs[0] ?? '';
120
- const note = refs.length > 1 ? ambiguousTestidNote(value) : undefined;
121
- session.beginAction?.(ReticleTool.FLOW_REPLAY, { ref, action: step.action ?? '' });
122
- let act;
123
- try {
124
- act = await session.command(ReticleCommand.ACT, {
125
- ref,
126
- action: step.action ?? '',
127
- // The field this step types into — from the anchor, so a redacted fill can be supplied from
128
- // RETICLE_SECRET_<FIELD> without the flow carrying the secret. The testid runner used to pass
129
- // the testid string here and the other two runners passed nothing, so a role-anchored login
130
- // typed the literal placeholder.
131
- args: replayActionArgs(step.args, confirmDangerous, anchorFieldName(step.anchor)),
132
- });
133
- }
134
- finally {
135
- session.finishAction?.();
117
+ /*
118
+ * More than one match is DRIFT, not a note on a passing step.
119
+ *
120
+ * This used to take refs[0], act, and return ok:true with `ambiguousTestidNote`. The verdict
121
+ * reads `drift` and `ok` and never reads a note, so "we guessed which element you meant" was
122
+ * indistinguishable from "it did what it did before" -- the one claim a replay makes.
123
+ *
124
+ * The action is NOT dispatched. Acting and then reporting drift would leave the app changed by
125
+ * a click nobody can attribute, which is worse than the ambiguity it reports.
126
+ */
127
+ if (refs.length > 1) {
128
+ return {
129
+ step: index,
130
+ tool: step.tool,
131
+ anchor: value,
132
+ ok: false,
133
+ drift: ambiguousAnchorDrift(value, refs.length),
134
+ };
136
135
  }
137
- const result = { step: index, tool: step.tool, anchor: value, ok: act.ok };
138
- if (!act.ok) {
139
- result.error = act.error ?? 'command failed';
140
- if (note !== undefined)
141
- result.note = note;
136
+ const ref = refs[0] ?? '';
137
+ /*
138
+ * The same dispatch its siblings use, including the one re-resolve on a stale ref (2.3).
139
+ *
140
+ * This runner had its own inline copy and no retry, so a flow died on a re-render purely because
141
+ * the step was anchored by testid rather than by role — the locator deciding how sturdy the replay
142
+ * is, which is exactly backwards. testid is also the anchor `reticle init` steers people towards,
143
+ * so the kind most likely to appear in a real flow was the kind without the cure.
144
+ *
145
+ * The re-resolve keeps the ambiguity rule above: more than one match is drift, never a guess, so
146
+ * it hands back a ref only when the locator still names exactly one element.
147
+ */
148
+ const result = await actOnResolvedRef(session, step, index, value, ref, confirmDangerous,
149
+ // The field this step types into — from the anchor, so a redacted fill can be supplied from
150
+ // RETICLE_SECRET_<FIELD> without the flow carrying the secret.
151
+ anchorFieldName(step.anchor), async () => {
152
+ const again = await resolveTestid(session, value, sleep);
153
+ return 1 === again.refs.length ? again.refs[0] : undefined;
154
+ });
155
+ if (!result.ok)
142
156
  return result;
143
- }
144
157
  // assert the step's expect.element testid is present AFTER the action —
145
158
  // unless that testid was marked DYNAMIC (the LLM-output case), in which case its presence/content
146
159
  // is NOT asserted (only the action ran). The skip is scoped strictly to the dynamic set.
147
- const expectTestid = step.expect?.element?.testid;
160
+ const expectTestid = expectedElementTestid(step.expect);
148
161
  if (expectTestid !== undefined && !dynamic.has(expectTestid)) {
149
162
  const expectRefs = await resolveTestid(session, expectTestid, sleep);
150
163
  if (0 === expectRefs.refs.length) {
@@ -160,8 +173,6 @@ async function runTestidStep(session, step, index, value, dynamic, confirmDanger
160
173
  };
161
174
  }
162
175
  }
163
- if (note !== undefined)
164
- result.note = note;
165
176
  return result;
166
177
  }
167
178
  /**
@@ -185,30 +196,52 @@ async function runTestidStep(session, step, index, value, dynamic, confirmDanger
185
196
  * quietly stop agreeing, and the disagreement would land as a flow that replays green against a
186
197
  * state it was never meant to run in.
187
198
  */
188
- export async function assertStepExpect(session, expect, dynamic, waitForSignal, timeoutMs, since) {
199
+ export async function assertStepExpect(session, expect, waitForSignal, timeoutMs, since) {
189
200
  // A testid is already asserted against the live DOM by the step runner. A role/name locator is
190
201
  // not that path — stripping every element made a recorded `until` by button name a no-op, so a
191
202
  // flow that proved the control at capture time could not go red when it was gone.
192
- const consequences = { ...expect };
193
- if (undefined !== consequences.element?.testid) {
194
- delete consequences.element;
195
- }
196
- const predicate = successToPredicate(consequences, dynamic);
203
+ /*
204
+ * Two clauses replay must not wait on, both for reasons that predate the tree.
205
+ *
206
+ * A testid element was already asserted against the live DOM by the step runner, so waiting on it
207
+ * again is a second read of a question already answered. A DYNAMIC testid is one whose content a
208
+ * model writes, and its presence is deliberately never asserted.
209
+ *
210
+ * A role/name locator is NEITHER, and stripping every element made a recorded `until` by button
211
+ * name a no-op — a flow that proved the control at capture time could not go red when it was gone.
212
+ *
213
+ * The DYNAMIC set used to be a parameter here and was dead the whole time: the testid was deleted
214
+ * from the expectation on the line above, and the only thing that read the set asked for the
215
+ * testid that had just been removed. One production caller was already passing an empty set. The
216
+ * rule is real and still applies to the flow's SUCCESS oracle, which is where it lives.
217
+ */
218
+ const predicate = withoutClauses(expect,
219
+ // Only a PRESENT testid is the runner's to check; an absence stays here or nobody checks it.
220
+ (clause) => PredicateKind.ELEMENT === clause.kind &&
221
+ clause.query.testid !== undefined &&
222
+ true !== clause.absent);
197
223
  if (predicate === undefined)
198
224
  return undefined;
199
225
  const verdict = await waitForSignal(session, predicate, timeoutMs, since);
200
226
  if (verdict.pass)
201
227
  return undefined;
228
+ // The wait ended because the page stopped being observable, not because the consequence failed:
229
+ // the same fact as a command the dying socket rejected, so it takes the same path (the replay loop
230
+ // reports a lost document). As a drift, a socket that blinked for 26ms read as the app's defect.
231
+ if (true === verdict.observationLost)
232
+ throw new Error(SESSION_DISCONNECTED_REASON);
202
233
  // A request the step NAMED that had not come back yet is a blind spot, not an assertion the app
203
234
  // failed. The live verdict path already draws this line (namedNetIsInFlight); replay read the
204
235
  // same window and never asked, so a slow endpoint was reported as a consequence that never fired.
205
236
  // An unrelated open request does not pardon a named URL that never started — matching decides it.
206
- const openRequests = expect.state === undefined ? inFlightRequestLabels(session.eventsSince(since)) : [];
237
+ const openRequests = assertsState(expect)
238
+ ? []
239
+ : inFlightRequestLabels(session.eventsSince(since), session.url, session.background);
207
240
  const namedInFlight = namedNetIsInFlight(predicate, openRequests);
208
241
  return {
209
242
  // The store case keeps its own kind because heal and the run report branch on it; everything
210
243
  // else is a consequence that did not hold, and the reason carries observed-vs-expected.
211
- reasonKind: expect.state !== undefined
244
+ reasonKind: assertsState(expect)
212
245
  ? DriftReason.STATE_MISMATCH
213
246
  : namedInFlight
214
247
  ? DriftReason.NET_STILL_IN_FLIGHT
@@ -220,21 +253,6 @@ export async function assertStepExpect(session, expect, dynamic, waitForSignal,
220
253
  nearest: null,
221
254
  };
222
255
  }
223
- /** Name the thing that was asserted, for the drift's `anchor` column. */
224
- function expectLabel(expect) {
225
- if (expect.signal !== undefined)
226
- return `signal:${expect.signal}`;
227
- if (expect.net !== undefined)
228
- return `net:${expect.net.urlContains ?? expect.net.method ?? '*'}`;
229
- if (expect.state !== undefined)
230
- return `state:${expect.state.path}`;
231
- if (expect.console !== undefined)
232
- return `console:${expect.console.level ?? '*'}`;
233
- if (undefined !== expect.element) {
234
- return expect.element.testid ?? expect.element.name ?? expect.element.role ?? 'element';
235
- }
236
- return 'expect';
237
- }
238
256
  /** Run one signal-anchored step: wait for the signal predicate, else drift (no nearest for signals). */
239
257
  async function runSignalStep(session, step, index, name, waitForSignal, signalTimeoutMs, since) {
240
258
  // Scope to THIS replay's floor, not the whole buffer.
@@ -262,40 +280,6 @@ async function runSignalStep(session, step, index, name, waitForSignal, signalTi
262
280
  },
263
281
  };
264
282
  }
265
- /**
266
- * Where to start REPORTING — and whether re-driving the steps before it is allowed at all.
267
- *
268
- * "Resume is nearly free, just re-run the prefix" is true of a browser and false on a subject that
269
- * COMMITS. Re-driving a prefix on a service re-sends every request before step N; on a device it
270
- * moves an arm again. Neither is a convenience, and a protocol that did it silently would be
271
- * defective rather than helpful.
272
- *
273
- * So the surface's declared profile decides, through the specification's own `resumeStrategy` rather
274
- * than a local reading of `replayPrefix`. A refusal resumes from 0 — the whole journey is reported,
275
- * nothing is skipped and nothing is silently repeated beyond what a plain replay already does.
276
- */
277
- function resumableFrom(options, steps) {
278
- const asked = Math.max(0, options.from ?? 0);
279
- if (0 === asked)
280
- return 0;
281
- if (!mayResumeByReplayingPrefix(options.surface ?? Surface.WEB))
282
- return 0;
283
- /*
284
- * The surface answers for the SUBJECT; a step answers for itself.
285
- *
286
- * `web` is the permissive profile and it is right about a browser in general and wrong about the
287
- * one click that charges a card. The surface check cannot see that, because the distinction is
288
- * not a property of the realm — it is a property of the step. A flow that declares a prefix step
289
- * as `commits` is saying: re-running me is not free, whatever the surface thinks.
290
- *
291
- * Refusing means resuming from 0, which is the same fail-safe the surface refusal already uses:
292
- * the whole journey is reported and nothing is silently repeated beyond what a plain replay does.
293
- * Absent effect is UNKNOWN and stays permissive — every flow recorded before this shipped has no
294
- * effect on any step, and assuming the worst there would refuse every resume in existence.
295
- */
296
- const commitsInPrefix = steps.slice(0, asked).some((step) => StepEffect.COMMITS === step.effect);
297
- return commitsInPrefix ? 0 : asked;
298
- }
299
283
  /**
300
284
  * Run an `invoke` step: load the named flow and replay it, or fail saying why.
301
285
  *
@@ -329,10 +313,7 @@ async function runInvokeStep(session, flow, step, index, waitForSignal, signalTi
329
313
  error: `cannot replay "${name}": it was not found, so this journey would report green having never run it`,
330
314
  };
331
315
  }
332
- // `from` is deliberately dropped rather than forwarded: it is a REPORTING offset into the
333
- // caller's own step list, and applying it inside a sub-journey would silently hide that
334
- // journey's first steps for a reason that has nothing to do with it.
335
- const { from: _ignored, ...carried } = options;
316
+ const carried = options;
336
317
  const nested = await replayFlow(session, sub, waitForSignal, signalTimeoutMs, confirmDangerous, sleep, {
337
318
  ...carried,
338
319
  via: [{ flow: flow.name, step: index }, ...via],
@@ -350,16 +331,6 @@ async function runInvokeStep(session, flow, step, index, waitForSignal, signalTi
350
331
  }
351
332
  export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs, confirmDangerous = false, sleep = realSleep, options = {}) {
352
333
  const results = [];
353
- /*
354
- * Where to start REPORTING. Everything before it still runs.
355
- *
356
- * Resuming is re-driving the prefix, not restoring state: there is no way to put an app back
357
- * where it was without driving it there, and at a measured ~27ms a step there is no reason to
358
- * try. So the prefix executes silently and the caller sees the journey from the point it asked
359
- * about -- which is what makes "fix the break, resume, find the next one" a loop rather than a
360
- * full re-read each time.
361
- */
362
- const from = resumableFrom(options, flow.steps);
363
334
  // testids whose region is LLM-dynamic — their expect-presence is NOT asserted.
364
335
  const dynamic = new Set((flow.dynamic ?? [])
365
336
  .filter((a) => a.kind === AnchorKind.TESTID)
@@ -436,7 +407,7 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
436
407
  for (const expectation of declared) {
437
408
  if (!result.ok || result.drift !== undefined)
438
409
  break;
439
- const expectDrift = await assertStepExpect(session, expectation, dynamic, waitForSignal, waitFor(step), cursorBefore);
410
+ const expectDrift = await assertStepExpect(session, expectation, waitForSignal, waitFor(step), cursorBefore);
440
411
  if (expectDrift !== undefined) {
441
412
  result.ok = false;
442
413
  result.drift = expectDrift;
@@ -466,8 +437,7 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
466
437
  // forget the rule and quietly re-introduce the cost. Spelled out only when it is NOT the default.
467
438
  if (FlowStepTool.ACT === result.tool)
468
439
  delete result.tool;
469
- if (index >= from || !result.ok || result.drift !== undefined)
470
- results.push(result);
440
+ results.push(result);
471
441
  // Under `sweep`, a failure whose action still RAN does not stop the run — the page is where the
472
442
  // step left it, so the next step is as meaningful as it was going to be. Anything else halts.
473
443
  const sweepPast = true === options.sweep &&
@@ -1,17 +1,26 @@
1
- /**
2
- * How one anchored step is turned into a live action.
3
- *
4
- * Split out of flow-replay.ts when adding the role+name runner pushed that file past the size cap.
5
- * These three share one shape — resolve an anchor to a ref, then dispatch — and the sharing is the
6
- * point: the action window must open and close identically no matter which anchor found the element,
7
- * or a step's events land in the wrong window depending on how it was addressed.
8
- */
9
- import { AnchorKind, type FlowAnchor, type FlowStep, type FlowStepResult } from '@reticlehq/core';
1
+ import { AnchorKind, type FlowAnchor, type FlowStep, type FlowStepResult, type Predicate } from '@reticlehq/core';
10
2
  import type { FlowReplaySession, Sleep } from './flow-replay-types.js';
11
3
  export declare function runRoleStep(session: FlowReplaySession, step: FlowStep, index: number, anchor: Extract<FlowAnchor, {
12
4
  kind: typeof AnchorKind.ROLE;
13
5
  }>, confirmDangerous: boolean, sleep: Sleep): Promise<FlowStepResult>;
14
- export declare function replayDestructiveActionHint(rawError: string): string;
6
+ /**
7
+ * Dispatch one already-resolved step. Shared so every anchor kind runs the action the same way —
8
+ * including the action window, whose open/close must not depend on which anchor found the element.
9
+ */
10
+ export declare function actOnResolvedRef(session: FlowReplaySession, step: FlowStep, index: number, label: string, ref: string, confirmDangerous: boolean,
11
+ /**
12
+ * The field name a redacted fill is supplied under, from `anchorFieldName` — the SAME function
13
+ * redaction uses to decide what to hide. Without it a role-anchored secret is redacted at save
14
+ * and looked up at replay under no name at all, so the flow types the placeholder into the form.
15
+ */
16
+ field?: string,
17
+ /**
18
+ * Resolve the anchor again. Supplied by the caller because only it knows how this step's anchor is
19
+ * found, and the retry below is worthless without it: re-dispatching the SAME dead ref fails
20
+ * identically. What changed between the two attempts is the DOM, so the locator has to be run
21
+ * against it again.
22
+ */
23
+ reresolve?: () => Promise<string | undefined>): Promise<FlowStepResult>;
15
24
  /** Run one component-anchored step: re-resolve via QUERY by:'component', ACT on the live ref, else drift. */
16
25
  export declare function runComponentStep(session: FlowReplaySession, step: FlowStep, index: number, anchor: Extract<FlowAnchor, {
17
26
  kind: typeof AnchorKind.COMPONENT;
@@ -39,6 +48,13 @@ export declare function degradedStepResult(step: FlowStep, index: number, label:
39
48
  * the step runner's own resolution asking the same thing of the same anchor.
40
49
  */
41
50
  export declare function anchorQueryArgs(anchor: FlowAnchor): Record<string, unknown> | null;
51
+ /**
52
+ * The anchor as a PRECONDITION a flow file can store: "this element is on the page".
53
+ *
54
+ * What `requires` means for a flow that starts from state a page load discards, in the shape the
55
+ * flow's own `requires` holds. No tool writes `requires`, so the reset's hint hands over this line.
56
+ */
57
+ export declare function anchorPrecondition(anchor: FlowAnchor): Predicate | undefined;
42
58
  /**
43
59
  * Run an act_sequence step: resolve every sub-step's OWN anchor, then dispatch the whole thing as one
44
60
  * ACT_SEQUENCE — the same shape `replayProgram` already uses, so a recorded sequence and a saved one
@@ -6,7 +6,8 @@
6
6
  * point: the action window must open and close identically no matter which anchor found the element,
7
7
  * or a step's events land in the wrong window depending on how it was addressed.
8
8
  */
9
- import { AnchorKind, DEGRADED_ANCHOR_ROLE, DriftReason, QueryBy, ReticleCommand, } from '@reticlehq/core';
9
+ import { expectedElementTestid } from '@reticlehq/core';
10
+ import { AnchorKind, DEGRADED_ANCHOR_ROLE, DriftReason, PredicateKind, QueryBy, ReticleCommand, } from '@reticlehq/core';
10
11
  import { ReticleTool } from '@reticlehq/core';
11
12
  import { replayActionArgs } from './replay.js';
12
13
  import { isStaleRefError } from '../../surface/tools/act/act-sequence-retry.js';
@@ -102,7 +103,7 @@ export async function runRoleStep(session, step, index, anchor, confirmDangerous
102
103
  * to recognise the raw refusal still finds it, plus the fix for this specific caller.
103
104
  */
104
105
  const DESTRUCTIVE_ACTION_PATTERN = /potentially destructive (?:\w+ )*(?:action|tool) blocked/i;
105
- export function replayDestructiveActionHint(rawError) {
106
+ function replayDestructiveActionHint(rawError) {
106
107
  if (!DESTRUCTIVE_ACTION_PATTERN.test(rawError))
107
108
  return rawError;
108
109
  return (`${rawError} — this is a flow REPLAY: a step's own args.confirmDangerous is stripped before ` +
@@ -118,7 +119,7 @@ export function replayDestructiveActionHint(rawError) {
118
119
  * Dispatch one already-resolved step. Shared so every anchor kind runs the action the same way —
119
120
  * including the action window, whose open/close must not depend on which anchor found the element.
120
121
  */
121
- async function actOnResolvedRef(session, step, index, label, ref, confirmDangerous,
122
+ export async function actOnResolvedRef(session, step, index, label, ref, confirmDangerous,
122
123
  /**
123
124
  * The field name a redacted fill is supplied under, from `anchorFieldName` — the SAME function
124
125
  * redaction uses to decide what to hide. Without it a role-anchored secret is redacted at save
@@ -252,6 +253,21 @@ export function anchorQueryArgs(anchor) {
252
253
  return roleQueryArgs(anchor);
253
254
  return null;
254
255
  }
256
+ /**
257
+ * The anchor as a PRECONDITION a flow file can store: "this element is on the page".
258
+ *
259
+ * What `requires` means for a flow that starts from state a page load discards, in the shape the
260
+ * flow's own `requires` holds. No tool writes `requires`, so the reset's hint hands over this line.
261
+ */
262
+ export function anchorPrecondition(anchor) {
263
+ if (anchor.kind === AnchorKind.TESTID) {
264
+ return { kind: PredicateKind.ELEMENT, query: { testid: anchor.value } };
265
+ }
266
+ if (anchor.kind === AnchorKind.ROLE && !isDegradedAnchor(anchor)) {
267
+ return { kind: PredicateKind.ELEMENT, query: { role: anchor.role, name: anchor.name } };
268
+ }
269
+ return undefined;
270
+ }
255
271
  /**
256
272
  * Run an act_sequence step: resolve every sub-step's OWN anchor, then dispatch the whole thing as one
257
273
  * ACT_SEQUENCE — the same shape `replayProgram` already uses, so a recorded sequence and a saved one
@@ -321,7 +337,7 @@ dynamic = new Set()) {
321
337
  };
322
338
  /** The sub-step's own expectation, unless its testid is deliberately unasserted. */
323
339
  const declaredBy = (sub) => {
324
- const testid = sub.expect?.element?.testid;
340
+ const testid = expectedElementTestid(sub.expect);
325
341
  return testid === undefined || dynamic.has(testid) ? undefined : testid;
326
342
  };
327
343
  /**
@@ -12,21 +12,36 @@
12
12
  * tool and the `@reticlehq/test` spec runner share one implementation — no divergent oracle.
13
13
  * Pure: no IO, no clock.
14
14
  */
15
- import { type FlowExpect, type FlowFile } from '@reticlehq/core';
16
- import type { EvalResult, Predicate } from '@reticlehq/engine/question/predicate/predicate.js';
15
+ import { type FlowExpect, type FlowFile, type Predicate } from '@reticlehq/core';
16
+ import type { EvalResult } from '@reticlehq/engine/question/predicate/predicate.js';
17
17
  import type { FlowReplaySession, WaitForSignal } from './flow-replay-types.js';
18
18
  /** The dynamic (LLM-output) testids whose presence is never asserted — same rule replay uses. */
19
19
  export declare function dynamicTestids(flow: FlowFile): Set<string>;
20
20
  /** The synthetic step.tool a replay appends when it asserts the flow's success oracle. */
21
21
  export declare const SUCCESS_STEP_TOOL = "success";
22
22
  /** A short human label for the success end-condition, for result rows. */
23
- export declare function successLabel(success: FlowExpect): string;
24
- /** Compile a success FlowExpect into a predicate. undefined → nothing assertable (vacuously met). */
25
- export declare function successToPredicate(success: FlowExpect, dynamic: ReadonlySet<string>): Predicate | undefined;
23
+ export declare function successLabel(success: Predicate | undefined): string;
24
+ /**
25
+ * What a flow's success oracle actually asserts, once the file has been read and the dynamic
26
+ * (LLM-output) testids have been taken out of it.
27
+ *
28
+ * Most of what this function used to do is gone, and that is the migration working as intended: it
29
+ * compiled a flat `FlowExpect` into a `Predicate`, and a step's `expect` IS a predicate now, lifted
30
+ * by the schema when the file is read. It keeps the name and the signature because it is on
31
+ * `@reticlehq/test`'s published surface, and it still accepts the flat shape for the same reason a
32
+ * v1 file still loads — a spec written against the old spelling is not wrong, it is old.
33
+ *
34
+ * What remains is the DYNAMIC SKIP, and it is not cosmetic. A testid whose content a model writes is
35
+ * asserted for PRESENCE by the step layer and never for content; a success oracle bound to one would
36
+ * assert exactly what the flow has already declared unassertable. Dropping every such clause can
37
+ * leave nothing at all, and `undefined` is the honest answer — vacuously met, the same as declaring
38
+ * no success, because nothing here was checkable.
39
+ */
40
+ export declare function successToPredicate(success: Predicate | FlowExpect | undefined, dynamic: ReadonlySet<string>): Predicate | undefined;
26
41
  /**
27
42
  * Assert a flow's success end-condition after replay. Delegates to the injected waitForSignal (the
28
43
  * real waitForPredicate in production, a fake in unit tests). Passes when: no success was declared,
29
44
  * OR every success field was dynamic-skipped (vacuously met), OR the compiled predicate held within
30
45
  * the injected timeout. Never reads the wall clock.
31
46
  */
32
- export declare function assertSuccess(session: FlowReplaySession, success: FlowExpect | undefined, dynamic: ReadonlySet<string>, waitForSignal: WaitForSignal, timeoutMs: number, since?: number): Promise<EvalResult>;
47
+ export declare function assertSuccess(session: FlowReplaySession, success: Predicate | undefined, dynamic: ReadonlySet<string>, waitForSignal: WaitForSignal, timeoutMs: number, since?: number): Promise<EvalResult>;