@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
package/docs/packages.mdx CHANGED
@@ -239,7 +239,7 @@ A lint rule makes the signal layer self-enforcing, which is the only way a conve
239
239
 
240
240
  ```toml
241
241
  [dependencies]
242
- reticle-tauri = "3.2"
242
+ reticle-tauri = "3.3"
243
243
  ```
244
244
 
245
245
  <Warning>
@@ -48,7 +48,17 @@ Take the advice. `element` and `text` are convenient and weak; `signal`, `state`
48
48
  }
49
49
  ```
50
50
 
51
- `query` accepts `role`, `name`, `text`, `label`, `placeholder`, `testid`, `alt`, `scope`. `state` is one of `visible`, `hidden`, `enabled`, `disabled`, `checked`, `expanded`, `focused`, `present`.
51
+ `query` accepts `role`, `name`, `text`, `label`, `placeholder`, `testid`, `alt`, `value`, `by`, `component`, `source`, `scope`, `self`, `attrs`. `state` is one of `visible`, `hidden`, `enabled`, `disabled`, `checked`, `expanded`, `focused`, `inViewport`, `present`.
52
+
53
+ `value` is the one worth knowing about. `{ "role": "textbox", "name": "Amount" }` collapses to "a textbox called Amount exists", which passes against an empty field; adding `value` is what makes it an assertion about what the user will actually send. `by` selects the locator strategy explicitly, `component` and `source` match React component identity and source location, and `self` restricts a scoped match to the scope element itself rather than its descendants.
54
+
55
+ `attrs` is a list of attribute **names**, such as `["href", "data-status"]`. It **projects** those attributes onto each match so you can read them back, and it does not filter. `["data-status=complete"]` is refused: it would otherwise look up an attribute literally named `data-status=complete`, find none, and leave the predicate resting on the locator alone, which is a green for any value the element happens to hold.
56
+
57
+ To READ the values, call `reticle_query` with `attrs` and look at the `attrs` map on each match. To ASSERT one: a form field's value goes in the query as `value`, while a state expressed as a data attribute is better asserted as the consequence it stands for, such as a `signal`, a `net` call, or the text the user actually sees.
58
+
59
+ ```json
60
+ { "kind": "element", "query": { "role": "textbox", "name": "Amount", "value": "274.58" } }
61
+ ```
52
62
 
53
63
  Add `"absent": true` to assert something is **gone**. The predicate for a removal, a dismissed toast, or a regression check.
54
64
 
@@ -62,6 +72,26 @@ Add `"absent": true` to assert something is **gone**. The predicate for a remova
62
72
 
63
73
  Use `scope` to restrict the search to a CSS selector or element ref. If Reticle reports that the text is split across the scope's children, retry its generated predicate with `"self": true`; that checks the scope root's combined subtree text while still enforcing `contains`.
64
74
 
75
+ `satisfies` asserts a PROPERTY of the rendered text instead of its exact bytes: the same shape and the same properties [`state` accepts](#satisfies-asserting-a-property-instead-of-exact-bytes), applied where the user actually reads the value:
76
+
77
+ ```json
78
+ {
79
+ "kind": "text",
80
+ "scope": "[data-testid=\"release-note\"]",
81
+ "self": true,
82
+ "satisfies": { "property": "nonEmpty" }
83
+ }
84
+ ```
85
+
86
+ That is the assertion a generated feature can pass twice. `contains` demands bytes a model's output does not repeat, so it either fails on correct output or gets weakened to a word so common it proves nothing; "the box rendered and there is something in it" is the failure the feature actually has.
87
+
88
+ Two rules, both refusals rather than guesses:
89
+
90
+ - **One of `contains` or `satisfies` is required.** With neither, the predicate matches any element with any text and cannot fail.
91
+ - **`satisfies` on its own needs a `scope`** (the element whose text is read, with `self: true` for the root's own subtree). Without one the locator is every element on the page, and the property would run against whichever matched first.
92
+
93
+ Both may be given, and then both must hold. `satisfies` narrows, it never excuses.
94
+
65
95
  ### net
66
96
 
67
97
  ```json
@@ -174,6 +204,72 @@ Omit `equals` entirely to assert presence. A real `$length` check against this f
174
204
 
175
205
  Note `grade: state` in the `because` line, and that the evidence collapses each row to a size marker rather than returning forty objects.
176
206
 
207
+ #### satisfies: asserting a property instead of exact bytes
208
+
209
+ `equals` cannot express output that is right differently every run. A generated summary, a classification, a computed total: each is correct without being predictable, and an assertion that demands exact bytes either fails on correct output or gets weakened until it proves nothing. `satisfies` is the other half of `state`, and it stays decidable here with no model and no network.
210
+
211
+ ```json
212
+ { "kind": "state", "store": "app", "path": "summary.text", "satisfies": { "property": "nonEmpty" } }
213
+ ```
214
+
215
+ | `property` | Extra fields | Holds when |
216
+ | --- | --- | --- |
217
+ | `nonEmpty` | none | it produced something at all, the honest floor for any output |
218
+ | `oneOf` | `values` | it landed inside the allowed set, exact membership, no coercion |
219
+ | `withinTolerance` | `of`, `tolerance` | a number near enough to the expected one, inclusive on the bound |
220
+ | `matchesPattern` | `pattern` | its string form matches that regular expression |
221
+ | `type` | `is` | it is that kind of thing; `array` and `object` are distinguished, which `typeof` cannot do |
222
+
223
+ ```json
224
+ {
225
+ "kind": "allOf",
226
+ "predicates": [
227
+ {
228
+ "kind": "state",
229
+ "path": "invoice.total",
230
+ "satisfies": { "property": "withinTolerance", "of": 274.58, "tolerance": 0.01 }
231
+ },
232
+ {
233
+ "kind": "state",
234
+ "path": "invoice.status",
235
+ "satisfies": { "property": "oneOf", "values": ["draft", "sent"] }
236
+ },
237
+ { "kind": "state", "path": "invoice.lines", "satisfies": { "property": "type", "is": "array" } }
238
+ ]
239
+ }
240
+ ```
241
+
242
+ `equals` and `satisfies` may both be given, and then both must hold. A failing check always says what it saw, because an assertion that reports only `false` sends you back for another call to find out why, and that round trip is most of what a verdict costs.
243
+
244
+ #### The relative properties: comparing with before
245
+
246
+ The properties above read the present tense. Four read the past one. They compare this value with the reading taken **before the action**, which is the window a verdict is already scoped to:
247
+
248
+ | property | holds when |
249
+ | ----------- | --------------------------- |
250
+ | `unchanged` | the value is the one it was |
251
+ | `changed` | it is not |
252
+ | `increased` | the number went up |
253
+ | `decreased` | the number went down |
254
+
255
+ ```json
256
+ {
257
+ "kind": "state",
258
+ "path": "cart.total",
259
+ "satisfies": { "property": "decreased", "by": { "op": "equals", "value": 12 } }
260
+ }
261
+ ```
262
+
263
+ `increased` and `decreased` take an optional `by`, in the protocol's own comparison vocabulary: `op` is `equals`, `at-least` or `at-most`, with an optional `tolerance` on all three. Without `by`, any movement in the right direction holds.
264
+
265
+ **Give a money assertion a tolerance.** `100 - 88.13` is `11.870000000000005`, so `{ "op": "equals", "value": 11.87 }` misses by a rounding error and reads as the app charging the wrong amount. `"tolerance": 0.01` is the fix; the failure message says so when the miss is that small.
266
+
267
+ A number is read out of displayed text, so `"$1,234.50"` compares as `1234.5`. A reading with no digits in it is **not** zero. `"sold out"` reports that nothing was subtracted rather than pretending the total fell to nothing.
268
+
269
+ These only work where a before-reading was taken, which today is `reticle_act_and_wait`. Asked anywhere else they report `unknown` with "no before-reading was taken", never a failure, because nothing was compared and a `false` would blame the app for a reading nobody took. For the same reason a relative property is never `already_true`: `unchanged` is trivially true against a baseline taken a microsecond earlier, so the pre-action check skips it and lets the action decide.
270
+
271
+ A saved flow cannot keep one yet: `FlowExpect` has no slot for `satisfies`, and rather than record a weaker assertion than the one you made, the converter records none.
272
+
177
273
  A miss quotes the real value back at you:
178
274
 
179
275
  ```json
@@ -207,6 +303,33 @@ This is the predicate that catches a UI-versus-store desync. A deploy that only
207
303
 
208
304
  Network and DOM idle. This is the deterministic replacement for a fixed sleep, not a proof of anything: combine it with a real consequence rather than asserting it alone. Omitting `until` on [`reticle_act_and_wait`](/tools/act-and-wait) waits for exactly this.
209
305
 
306
+ ## Asserting a failure on purpose
307
+
308
+ A fault-injection test (mock a 500, check the error UI) is asserting that a request **fails**. Say so in the predicate, or the verdict will disagree with you:
309
+
310
+ ```json
311
+ {
312
+ "kind": "allOf",
313
+ "predicates": [
314
+ { "kind": "text", "contains": "Something went wrong" },
315
+ { "kind": "net", "method": "POST", "urlContains": "/api/save", "status": 503 }
316
+ ]
317
+ }
318
+ ```
319
+
320
+ Asserting only the error text is not enough, and this is the most common way to get a confusing `verified: "no"`. Reticle watches for "the UI moved forward while a request failed", a swallowed rejection, and one of the most expensive bugs it catches. A fault-injection test looks **exactly** like that bug from the outside: a request failed, the screen changed, nothing said that was the plan.
321
+
322
+ Naming the failing call is what tells the two apart. A `net` clause with `status` of 400 or more, or `"ok": false`, which is the honest field for IPC, where there is no status code, marks that call as expected to fail, and a failure there stops being a disagreement.
323
+
324
+ <Warning>
325
+ Declare it in the top level or inside `allOf`. A declaration inside `anyOf` is deliberately
326
+ ignored: only one branch of an `anyOf` has to hold, so honouring it would suppress a real
327
+ contradiction on the strength of a branch that never ran. `not` is ignored for the same reason in
328
+ reverse: `not { net status: 500 }` asserts the call did *not* fail.
329
+ </Warning>
330
+
331
+ One case is inferred for you: an `element` or `text` clause naming an access denial ("not authorised", "sign in to continue") marks the auth statuses as expected, because a denial screen is a correct outcome rather than a fault. Everything else has to be named.
332
+
210
333
  ## Combinators
211
334
 
212
335
  ```json
@@ -226,7 +349,8 @@ Network and DOM idle. This is the deterministic replacement for a fixed sleep, n
226
349
  ```
227
350
 
228
351
  <Warning>
229
- A combinator needs its own `kind`, and the children go in `predicates`. A bare
352
+ A combinator needs its own `kind`, and the children go in `predicates`, or in `of`, which is
353
+ accepted as an alias for it. `not` takes a single child in `predicate`, or in `of`. A bare
230
354
  `{ "allOf": [ … ] }` does not parse, and neither does `{ "kind": "allOf", "allOf": [ … ] }`.
231
355
 
232
356
  Reticle refuses the call rather than running half of it. The real refusal names the field, the fix and the consequence: _"predicates: Required; unknown field allOf. Nothing ran, the predicate was not evaluated, so no verdict was produced. allOf accepts: predicates."_ Better to be told than to get a green from an assertion that never executed.
@@ -265,9 +389,9 @@ On a failure each child reports separately too, so you can see which one broke:
265
389
  | Field | Where | What it does |
266
390
  | --- | --- | --- |
267
391
  | `timeout_ms` | `assert`, `wait_for`, `act_and_wait` | Wait up to N ms for it to become true |
268
- | `since` | `net` and `console` leaves, and the call itself | Only consider events after this cursor |
392
+ | `since` | `net`, `route`, `console`, `animation` and `signal` leaves, and the call itself | Only consider events after this cursor |
269
393
 
270
- The `since` cursor comes from a prior `reticle_act` response. Use it whenever you are asserting something an action was supposed to cause. Without it, an assertion can pass on an event from two clicks ago.
394
+ The `since` cursor comes from a prior `reticle_act` response. Use it whenever you are asserting something an action was supposed to cause. Without it, an assertion can pass on an event from two clicks ago. That applies to every one of the five kinds above, not only to network calls: an unscoped `signal` matches a fire from an earlier action just as readily.
271
395
 
272
396
  <Card title="Where predicates get used" icon="circle-check" href="/tools/act-and-wait">
273
397
  Naming the consequence before the action is the whole point.
@@ -52,16 +52,16 @@ Seven buttons, each with a `ref` to act on and a source file to open, for 236 to
52
52
 
53
53
  ## Arguments
54
54
 
55
- | Argument | What it does |
56
- | ------------ | -------------------------------------------------------------------------- |
57
- | `by` | `role` · `text` · `label` · `placeholder` · `testid` · `alt` · `component` |
58
- | `value` | The query value for that strategy |
59
- | `name` | Accessible-name filter. Narrows a broad `role` query |
60
- | `scope` | CSS selector or ref, to search inside a subtree |
61
- | `limit` | Cap the descriptors returned. Cuts tokens hard on broad queries |
62
- | `count_only` | Return just `{ count }`, around 30x smaller |
63
- | `attrs` | Extra attributes per match, e.g. `['href']` to inventory links |
64
- | `self` | Return the `scope` element itself instead of searching inside it |
55
+ | Argument | What it does |
56
+ | --- | --- |
57
+ | `by` | `role` · `text` · `label` · `placeholder` · `testid` · `alt` · `component` |
58
+ | `value` | The query value for that strategy |
59
+ | `name` | Accessible-name filter. Narrows a broad `role` query |
60
+ | `scope` | CSS selector or ref, to search inside a subtree |
61
+ | `limit` | Cap the descriptors returned. Cuts tokens hard on broad queries |
62
+ | `count_only` | Return just `{ count }`, around 30x smaller |
63
+ | `attrs` | Attribute NAMES to project per match, e.g. `['href']` to inventory links. The values come back in each match's `attrs` map. It does not filter, so `name=value` is refused |
64
+ | `self` | Return the `scope` element itself instead of searching inside it |
65
65
 
66
66
  Every argument also has a predicate spelling, so `{ "role": "button" }` works as shorthand for `{ "by": "role", "value": "button" }`.
67
67
 
package/docs/usage.md CHANGED
@@ -200,7 +200,7 @@ Verify a predicate; optionally wait for it.
200
200
  - **args:** `predicate`, `timeout_ms?` (0 = evaluate once), `since?`, `intent?`, `sessionId?`.
201
201
  - Same `since` default as `reticle_assert { action: "wait" }`: scoped to your last act so a stale buffered event can't fake a pass; override with an explicit `since`.
202
202
  - **returns:** `{ verified, because, pass, evidence, contradictions?, coverage?, failureReason?, session, warning? }`. On failure includes a **near-miss** (e.g. "found the dialog but not visible", or "no button named 'Submit'; saw: Cancel"). The `session` block `{ lastSeenMs, throttled, focused }` reports tab health on every assert; when throttled a `warning` is attached so you never assert against a tab that is silently no-oping.
203
- - **`intent` declares what the change was FOR, inline.** Pass a sentence in your own words and it lands in `.reticle/intent.json`, the same git-checked ledger `reticle_intent` writes, before the verdict is drawn; a green verdict then marks it proved and names itself as the proof. Pass the **id** of an intent you already declared instead, to point several verdicts at one statement rather than restating it. `reticle_act_and_wait` takes the same argument. Omit it and nothing is written.
203
+ - **`intent` declares what the change was FOR, inline.** Pass a sentence in your own words and it lands in `.reticle/intent/` (one directory per subject, a flow's name being its subject), the same git-checked ledger `reticle_intent` writes, before the verdict is drawn; a green verdict then marks it proved and names itself as the proof. Pass the **id** of an intent you already declared instead, to point several verdicts at one statement rather than restating it. `reticle_act_and_wait` takes the same argument. Omit it and nothing is written.
204
204
  - **Read `verified`, not `pass`.** `pass` says the predicate held; `verified` says whether that means anything. It is `"no"` when a channel contradicts the assertion (a failed write under a green screen, a batch whose body reports per-item failures, a request still in flight), and `"unknown"` when the outcome could not be known yet: a `202 Accepted` that has not reconciled, or a write whose response body was never recorded. `because` names the deciding evidence in one sentence.
205
205
 
206
206
  ### `reticle_reconcile`
@@ -0,0 +1,62 @@
1
+ ---
2
+ title: What is recorded
3
+ description: What Reticle keeps on your machine, what is redacted, and what it takes for any of it to leave.
4
+ ---
5
+
6
+ Reticle drives your app and reads what it did. That means it holds real content from real pages, so this page says exactly what it keeps, where it keeps it, and what it takes to make any of it leave.
7
+
8
+ If you read one line: **a verdict is entirely local and needs no account.** Nothing is sent anywhere until you run `reticle login` and `reticle link`. Without them there is nowhere for anything to go.
9
+
10
+ ## On your machine
11
+
12
+ Everything lands in `.reticle/` in your project, and it is yours. Three kinds of thing:
13
+
14
+ | | what it is | what is in it |
15
+ | --- | --- | --- |
16
+ | **runs** | a verdict and its evidence | what was claimed, whether it held, the request or state change that decided it, and the `file:line` of the element driven |
17
+ | **flows** | a journey you drove, saved so it can be replayed | the steps, the anchors they resolve by, and the values that were typed |
18
+ | **memory** | what this project has learned across sessions | intents you declared, envelopes of normal behaviour, notes about anchors that drift |
19
+
20
+ `.reticle/` splits into a part meant for git and a part meant to stay local. Flows are the part worth committing: a saved flow is a regression test. Evidence is the part that is not.
21
+
22
+ ## What is redacted, and what is not
23
+
24
+ **Credential-shaped fields are redacted when a flow is saved.** A value typed into a field whose name looks like a secret is replaced with `<redacted: supply at replay>`, and replay reads the real value from `RETICLE_SECRET_<FIELD>` at run time. It keys off the field's name across every anchor kind (a testid, an accessible name, a signal), so an app without test ids is covered too.
25
+
26
+ **It is name-based, and that is the limit worth knowing.** It redacts a password. It does not redact a customer's name typed into a search box, because nothing about that field says "secret". A flow records the journey you actually drove.
27
+
28
+ So: **drive staging.** Not because the risk is large, but because it is real and avoiding it is free.
29
+
30
+ Network bodies are **not** captured by default. Page content that reaches a verdict is the element text and attributes the assertion needed, not a copy of the DOM.
31
+
32
+ ## What leaves, only if you ask
33
+
34
+ `reticle login` signs this machine in. `reticle link` binds one repo to one cloud project. Until both have happened, `reticle push` has nothing to talk to and the sync path is a no-op. The code calls this the no-phone-home default.
35
+
36
+ Once linked, you choose what syncs:
37
+
38
+ ```bash
39
+ npx @reticlehq/server config --runs on|off --flows on|off --memory on|off
40
+ ```
41
+
42
+ ## Telemetry, which is separate from all of the above
43
+
44
+ The CLI sends anonymous usage events: event names and a random installation id. No code, no page content, no URLs from your app. It is how we know which parts of the product are reached at all.
45
+
46
+ ```bash
47
+ RETICLE_TELEMETRY=0 # this shell
48
+ DO_NOT_TRACK=1 # the convention, honoured
49
+ npx @reticlehq/server telemetry disable # this machine, permanently
50
+ ```
51
+
52
+ Feedback is the only free text that ever leaves, it is never collected passively, and `RETICLE_FEEDBACK=0` disables it.
53
+
54
+ ## What `init` writes to your project
55
+
56
+ Run it with `--dry-run` and it prints the whole plan without writing anything:
57
+
58
+ ```bash
59
+ npx @reticlehq/server init --dry-run
60
+ ```
61
+
62
+ `--app <dir>` picks which app in a monorepo. `--no-mcp` skips registering the MCP server with your agents and skips the agent rule files with it. `--files-only` writes the files and stops, without booting your dev server.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reticlehq/server",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "Reticle bridge + MCP server. Hosts the browser WS endpoint and exposes MCP tools to the coding agent.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -23,11 +23,11 @@
23
23
  "ws": "^8.21.3",
24
24
  "zod": "^3.24.1",
25
25
  "zod-to-json-schema": "^3.24.0",
26
- "@reticlehq/browser": "3.2.0",
27
- "@reticlehq/engine": "3.2.0",
28
- "@reticlehq/init": "3.2.0",
29
- "@reticlehq/core": "3.2.0",
30
- "open-verification": "1.0.0"
26
+ "@reticlehq/engine": "3.3.0",
27
+ "@reticlehq/browser": "3.3.0",
28
+ "@reticlehq/init": "3.3.0",
29
+ "open-verification": "1.0.0",
30
+ "@reticlehq/core": "3.3.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/pngjs": "^6.0.5",
@@ -1,44 +0,0 @@
1
- /**
2
- * The assertion the agent MADE, in the shape a saved flow can keep.
3
- *
4
- * `reticle_act_and_wait { until }` is how agents assert — nearly every asserting call in a day of telemetry
5
- * carried an `until`. `compileActStep` recorded only the ACTION, so a flow saved after an asserted
6
- * drive came back graded `assertion-free`: "performs actions but asserts no observable consequence —
7
- * it will pass even if the feature is broken." The agent had already said what success meant;
8
- * Reticle discarded it and then warned the agent that the flow asserts nothing.
9
- *
10
- * That is the regression-suite story failing at its last step. "Record once, verify forever" is only
11
- * worth anything if the recorded flow can go RED, and locally 32 of 39 saved flows cannot.
12
- *
13
- * The inverse of `successToPredicate`. Only the kinds FlowExpect can express are carried: `settled`
14
- * is a wait rather than a claim, and `animation`/`anyOf`/`not` have no representation at all (`route` gained one — see its case below).
15
- * Inventing one would write an assertion into the file that the agent never made, which is worse
16
- * than recording none — a flow that asserts something nobody chose is a false green with extra steps.
17
- */
18
- import type { FlowExpect } from '@reticlehq/core';
19
- import type { Predicate } from '@reticlehq/engine/question/predicate/predicate.js';
20
- export declare function predicateToExpect(predicate: Predicate): FlowExpect | undefined;
21
- /**
22
- * The subset a REPLAY actually enforces.
23
- *
24
- * The rule has never changed: never write an assertion into a flow file that nothing evaluates. A
25
- * flow reporting `grade: "asserted"` while its assertion is read by no one is a false green, in the
26
- * feature whose entire purpose is preventing them.
27
- *
28
- * What changed is the SET. This kept only `element.testid` and `state`, because for a long time
29
- * those were the only kinds replay checked — and the note here said lifting it "requires replay to
30
- * evaluate them per step". Replay now does exactly that: `assertStepExpect` compiles every
31
- * remaining kind through `successToPredicate` and waits on it. The condition was met and the filter
32
- * was not updated, so the two drifted.
33
- *
34
- * That drift had a cost, and it fell on the agent. `reticle_act_and_wait { until }` IS the agent
35
- * saying what success means, and `net` is overwhelmingly what it says. Every one of those was
36
- * discarded at capture, so an agent-recorded flow reached disk assertion-free BY CONSTRUCTION and
37
- * `reticle_verify` then correctly called it `unverifiable` — the record → save → verify path
38
- * completing all the way to a run that could never be a pass. Found by driving it.
39
- *
40
- * The list below is exactly what `successToPredicate` reads, including an element located by
41
- * role or name. A testid is also asserted directly against the DOM by the step runner. Anything
42
- * it cannot compile is still dropped.
43
- */
44
- export declare function enforcedOnReplay(expect: FlowExpect | undefined): FlowExpect | undefined;
@@ -1,148 +0,0 @@
1
- import { PredicateKind } from '@reticlehq/core';
2
- /** Merge two partial expectations; later keys win only where the earlier one said nothing. */
3
- function merge(into, from) {
4
- return { ...from, ...into };
5
- }
6
- export function predicateToExpect(predicate) {
7
- switch (predicate.kind) {
8
- case PredicateKind.SIGNAL: {
9
- if (predicate.name === undefined)
10
- return undefined;
11
- const signal = { signal: predicate.name };
12
- if (predicate.dataMatches !== undefined)
13
- signal.signalData = predicate.dataMatches;
14
- if (predicate.count !== undefined)
15
- signal.signalCount = predicate.count;
16
- return signal;
17
- }
18
- case PredicateKind.ROUTE: {
19
- // Carried, not invented: the agent said "until the route is X" in so many words. The kinds
20
- // below this comment's twin in the header — animation, anyOf, not — stay refused because
21
- // there the file would gain a claim nobody made.
22
- const route = {};
23
- if (predicate.pathname !== undefined)
24
- route.pathname = predicate.pathname;
25
- if (predicate.contains !== undefined)
26
- route.contains = predicate.contains;
27
- return 0 === Object.keys(route).length ? undefined : { route };
28
- }
29
- case PredicateKind.NET: {
30
- const net = {};
31
- if (predicate.method !== undefined)
32
- net.method = predicate.method;
33
- if (predicate.urlContains !== undefined)
34
- net.urlContains = predicate.urlContains;
35
- if (predicate.status !== undefined)
36
- net.status = predicate.status;
37
- if (predicate.count !== undefined)
38
- net.count = predicate.count;
39
- return 0 === Object.keys(net).length ? undefined : { net };
40
- }
41
- case PredicateKind.CONSOLE: {
42
- // `contains` has no representation in FlowExpect.console (core keeps `level` and `absent`
43
- // and nothing else). Copying the rest through anyway would record an assertion the agent
44
- // never made: {console, level:'warn', contains:'no-op', absent:true} would save "no warn
45
- // entries at all", a false red on any unrelated warning, and in the presence direction it
46
- // saves a strictly weaker claim than the one chosen. This file's own rule applies: record
47
- // nothing rather than something different, so the flow stays assertion-free there instead.
48
- if (predicate.contains !== undefined)
49
- return undefined;
50
- const console_ = {};
51
- if (predicate.level !== undefined)
52
- console_.level = predicate.level;
53
- if (predicate.absent !== undefined)
54
- console_.absent = predicate.absent;
55
- return 0 === Object.keys(console_).length ? undefined : { console: console_ };
56
- }
57
- case PredicateKind.ELEMENT: {
58
- const element = {};
59
- if (predicate.query.testid !== undefined)
60
- element.testid = predicate.query.testid;
61
- if (predicate.query.role !== undefined)
62
- element.role = predicate.query.role;
63
- if (predicate.query.name !== undefined)
64
- element.name = predicate.query.name;
65
- return 0 === Object.keys(element).length ? undefined : { element };
66
- }
67
- case PredicateKind.STATE: {
68
- const state = { path: predicate.path };
69
- if (predicate.store !== undefined)
70
- state.store = predicate.store;
71
- if (predicate.equals !== undefined)
72
- state.equals = predicate.equals;
73
- return { state };
74
- }
75
- case PredicateKind.ALL_OF: {
76
- // `settled` members drop out on their own by returning undefined.
77
- let combined;
78
- for (const part of predicate.predicates) {
79
- const expect = predicateToExpect(part);
80
- if (expect === undefined)
81
- continue;
82
- combined = combined === undefined ? expect : merge(combined, expect);
83
- }
84
- return combined;
85
- }
86
- default:
87
- // settled | route | animation | anyOf | not — nothing FlowExpect can say honestly.
88
- return undefined;
89
- }
90
- }
91
- /**
92
- * The subset a REPLAY actually enforces.
93
- *
94
- * The rule has never changed: never write an assertion into a flow file that nothing evaluates. A
95
- * flow reporting `grade: "asserted"` while its assertion is read by no one is a false green, in the
96
- * feature whose entire purpose is preventing them.
97
- *
98
- * What changed is the SET. This kept only `element.testid` and `state`, because for a long time
99
- * those were the only kinds replay checked — and the note here said lifting it "requires replay to
100
- * evaluate them per step". Replay now does exactly that: `assertStepExpect` compiles every
101
- * remaining kind through `successToPredicate` and waits on it. The condition was met and the filter
102
- * was not updated, so the two drifted.
103
- *
104
- * That drift had a cost, and it fell on the agent. `reticle_act_and_wait { until }` IS the agent
105
- * saying what success means, and `net` is overwhelmingly what it says. Every one of those was
106
- * discarded at capture, so an agent-recorded flow reached disk assertion-free BY CONSTRUCTION and
107
- * `reticle_verify` then correctly called it `unverifiable` — the record → save → verify path
108
- * completing all the way to a run that could never be a pass. Found by driving it.
109
- *
110
- * The list below is exactly what `successToPredicate` reads, including an element located by
111
- * role or name. A testid is also asserted directly against the DOM by the step runner. Anything
112
- * it cannot compile is still dropped.
113
- */
114
- export function enforcedOnReplay(expect) {
115
- if (expect === undefined)
116
- return undefined;
117
- const kept = {};
118
- // The step runner asserts a testid against the live DOM before the predicate engine. Role and
119
- // name are not that path: successToPredicate compiles them, and dropping them here made a
120
- // recorded `until` by button name vanish so the saved flow could not go red.
121
- const element = expect.element;
122
- if (undefined !== element &&
123
- (undefined !== element.testid || undefined !== element.role || undefined !== element.name)) {
124
- kept.element = element;
125
- }
126
- // Everything `successToPredicate` can compile. Replay evaluates EVERY kind of expect through it
127
- // (see assertStepExpect); this list is what that function actually reads.
128
- if (expect.state !== undefined)
129
- kept.state = expect.state;
130
- if (expect.signal !== undefined)
131
- kept.signal = expect.signal;
132
- if (expect.signalData !== undefined)
133
- kept.signalData = expect.signalData;
134
- if (expect.signalCount !== undefined)
135
- kept.signalCount = expect.signalCount;
136
- if (expect.net !== undefined)
137
- kept.net = expect.net;
138
- if (expect.console !== undefined)
139
- kept.console = expect.console;
140
- // `route` joined the list the day FlowExpect gained the field. This filter and
141
- // `successToPredicate` are two allowlists for one question — "can replay check this?" — and the
142
- // note above records them drifting once already. They drifted again here, in the same direction,
143
- // within an hour: the field was added, the mapping was added, and a route assertion still reached
144
- // disk as nothing because THIS list had not heard of it. Found by driving, not by a test.
145
- if (expect.route !== undefined)
146
- kept.route = expect.route;
147
- return 0 === Object.keys(kept).length ? undefined : kept;
148
- }