@reticlehq/server 2.6.0 → 2.8.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 (276) hide show
  1. package/README.md +14 -5
  2. package/SKILL.md +251 -33
  3. package/dist/bridge/bridge.d.ts +6 -1
  4. package/dist/bridge/bridge.js +27 -4
  5. package/dist/capsule/causal-summary.d.ts +20 -0
  6. package/dist/capsule/causal-summary.js +31 -6
  7. package/dist/capsule/divergence.js +7 -1
  8. package/dist/cli/chromium-hint.d.ts +68 -0
  9. package/dist/cli/chromium-hint.js +145 -0
  10. package/dist/cli/cli-doctor.js +31 -36
  11. package/dist/cli/cli-kill.d.ts +58 -0
  12. package/dist/cli/cli-kill.js +162 -0
  13. package/dist/cli/cli-launch.d.ts +18 -0
  14. package/dist/cli/cli-launch.js +32 -1
  15. package/dist/cli/cli-parse.d.ts +16 -1
  16. package/dist/cli/cli-parse.js +32 -9
  17. package/dist/cli/cli-port.d.ts +8 -0
  18. package/dist/cli/cli-port.js +36 -1
  19. package/dist/cli/cli-verify.js +2 -2
  20. package/dist/cli/doctor-daemon-line.js +10 -1
  21. package/dist/cli/doctor-sessions-line.d.ts +12 -0
  22. package/dist/cli/doctor-sessions-line.js +66 -0
  23. package/dist/cli/port-holder.d.ts +10 -0
  24. package/dist/cli/port-holder.js +26 -2
  25. package/dist/cli.js +139 -23
  26. package/dist/crawl/crawl-tools.js +12 -2
  27. package/dist/crawl/crawl.d.ts +13 -0
  28. package/dist/crawl/crawl.js +50 -2
  29. package/dist/daemon/daemon-resilience.js +2 -2
  30. package/dist/daemon/loopback-alias.d.ts +32 -0
  31. package/dist/daemon/loopback-alias.js +80 -0
  32. package/dist/events/contradictions.d.ts +14 -0
  33. package/dist/events/contradictions.js +26 -17
  34. package/dist/events/predicate-eval.d.ts +26 -3
  35. package/dist/events/predicate-eval.js +284 -95
  36. package/dist/events/predicate-parse.d.ts +1 -1
  37. package/dist/events/predicate-parse.js +18 -4
  38. package/dist/events/predicate.js +170 -18
  39. package/dist/events/ring-buffer.d.ts +8 -0
  40. package/dist/events/ring-buffer.js +34 -1
  41. package/dist/flows/flow-classify.js +1 -1
  42. package/dist/flows/flow-tools.js +12 -4
  43. package/dist/flows/heal-run.js +1 -1
  44. package/dist/honesty/blind-spots.js +6 -2
  45. package/dist/honesty/honesty.d.ts +11 -0
  46. package/dist/honesty/honesty.js +9 -1
  47. package/dist/honesty/unsettled.d.ts +40 -0
  48. package/dist/honesty/unsettled.js +78 -0
  49. package/dist/honesty/verified.d.ts +7 -0
  50. package/dist/honesty/verified.js +27 -4
  51. package/dist/http-server.js +11 -1
  52. package/dist/index.js +29 -2
  53. package/dist/init/agent-rules.d.ts +16 -0
  54. package/dist/init/agent-rules.js +96 -17
  55. package/dist/init/astro-patch.d.ts +2 -1
  56. package/dist/init/astro-patch.js +11 -5
  57. package/dist/init/capabilities.d.ts +22 -0
  58. package/dist/init/capabilities.js +76 -0
  59. package/dist/init/confirm.d.ts +96 -0
  60. package/dist/init/confirm.js +138 -0
  61. package/dist/init/cra.js +3 -2
  62. package/dist/init/detect.d.ts +8 -0
  63. package/dist/init/detect.js +15 -0
  64. package/dist/init/non-js-project.d.ts +27 -0
  65. package/dist/init/non-js-project.js +84 -0
  66. package/dist/init/plan-framework.d.ts +9 -0
  67. package/dist/init/plan-framework.js +72 -16
  68. package/dist/init/plan.d.ts +40 -4
  69. package/dist/init/plan.js +183 -43
  70. package/dist/init/run.d.ts +24 -2
  71. package/dist/init/run.js +181 -46
  72. package/dist/init/slash-command.d.ts +1 -1
  73. package/dist/init/slash-command.js +46 -6
  74. package/dist/init/snippets.d.ts +64 -5
  75. package/dist/init/snippets.js +220 -31
  76. package/dist/init/vite-config.js +29 -3
  77. package/dist/input/network-mock-tools.d.ts +19 -0
  78. package/dist/input/network-mock-tools.js +29 -4
  79. package/dist/license/license.js +1 -1
  80. package/dist/mcp/mcp-proxy.d.ts +69 -3
  81. package/dist/mcp/mcp-proxy.js +215 -23
  82. package/dist/mcp/mcp.js +58 -7
  83. package/dist/mcp/proxy-lifecycle.d.ts +20 -1
  84. package/dist/mcp/proxy-lifecycle.js +22 -1
  85. package/dist/pool/browser-pool.d.ts +22 -2
  86. package/dist/pool/browser-pool.js +44 -4
  87. package/dist/pool/playwright-launcher.js +10 -2
  88. package/dist/project/learned-routes.d.ts +24 -0
  89. package/dist/project/learned-routes.js +63 -0
  90. package/dist/project/project-store.d.ts +6 -0
  91. package/dist/project/project-store.js +25 -1
  92. package/dist/session/live-control-tools.js +39 -2
  93. package/dist/session/live-control.d.ts +7 -9
  94. package/dist/session/live-control.js +34 -1
  95. package/dist/session/no-session-diagnosis.d.ts +23 -4
  96. package/dist/session/no-session-diagnosis.js +128 -44
  97. package/dist/session/no-session-watch.js +12 -0
  98. package/dist/session/ref-provenance.d.ts +54 -0
  99. package/dist/session/ref-provenance.js +61 -0
  100. package/dist/session/review-store.d.ts +19 -0
  101. package/dist/session/review-store.js +34 -5
  102. package/dist/session/session-lease.js +1 -1
  103. package/dist/session/session-manager.d.ts +9 -0
  104. package/dist/session/session-manager.js +11 -0
  105. package/dist/session/session.d.ts +8 -0
  106. package/dist/session/session.js +12 -0
  107. package/dist/status-payload.d.ts +13 -1
  108. package/dist/status-payload.js +6 -1
  109. package/dist/telemetry/app-instrumented.d.ts +37 -0
  110. package/dist/telemetry/app-instrumented.js +54 -0
  111. package/dist/telemetry/automation-hint.d.ts +17 -0
  112. package/dist/telemetry/automation-hint.js +71 -0
  113. package/dist/telemetry/bug-found.d.ts +0 -20
  114. package/dist/telemetry/bug-found.js +38 -21
  115. package/dist/telemetry/cli-telemetry.js +7 -1
  116. package/dist/telemetry/daemon-telemetry.d.ts +4 -4
  117. package/dist/telemetry/daemon-telemetry.js +8 -4
  118. package/dist/telemetry/feedback.js +1 -1
  119. package/dist/telemetry/init-telemetry.js +9 -1
  120. package/dist/telemetry/install-source.d.ts +36 -0
  121. package/dist/telemetry/install-source.js +40 -0
  122. package/dist/telemetry/run-telemetry.js +4 -1
  123. package/dist/telemetry/session-metrics.d.ts +3 -3
  124. package/dist/telemetry/session-metrics.js +9 -4
  125. package/dist/telemetry/telemetry.d.ts +7 -1
  126. package/dist/telemetry/telemetry.js +17 -2
  127. package/dist/telemetry/tool-refused.d.ts +19 -0
  128. package/dist/telemetry/tool-refused.js +55 -0
  129. package/dist/telemetry/verification-of.js +72 -8
  130. package/dist/temp-dir.d.ts +27 -0
  131. package/dist/temp-dir.js +46 -0
  132. package/dist/tools/act-tools.js +77 -11
  133. package/dist/tools/assert-grade.d.ts +1 -1
  134. package/dist/tools/assert-grade.js +27 -5
  135. package/dist/tools/assert-verdict.js +25 -1
  136. package/dist/tools/contract-tools.js +4 -0
  137. package/dist/tools/dynamic-tools.js +1 -1
  138. package/dist/tools/error-recovery.d.ts +10 -0
  139. package/dist/tools/error-recovery.js +70 -5
  140. package/dist/tools/feedback-invite.js +1 -1
  141. package/dist/tools/invoke-tool.js +149 -32
  142. package/dist/tools/lease-tools.d.ts +29 -1
  143. package/dist/tools/lease-tools.js +58 -5
  144. package/dist/tools/navigate-result.js +1 -1
  145. package/dist/tools/observe-tools.js +1 -1
  146. package/dist/tools/observed-nothing.js +1 -1
  147. package/dist/tools/react-grace.d.ts +48 -0
  148. package/dist/tools/react-grace.js +87 -0
  149. package/dist/tools/read-tools.js +20 -4
  150. package/dist/tools/settle-in-flight.d.ts +79 -0
  151. package/dist/tools/settle-in-flight.js +106 -0
  152. package/dist/tools/snapshot-delta.d.ts +7 -0
  153. package/dist/tools/snapshot-delta.js +130 -6
  154. package/dist/tools/tool-kit.d.ts +52 -5
  155. package/dist/tools/tool-kit.js +55 -24
  156. package/dist/tools/tool-surface.js +28 -9
  157. package/dist/tools/tools.js +38 -4
  158. package/dist/visual/capture-cleanup.d.ts +16 -0
  159. package/dist/visual/capture-cleanup.js +39 -0
  160. package/dist/visual/visual-tools.d.ts +18 -0
  161. package/dist/visual/visual-tools.js +24 -5
  162. package/docs/README.md +56 -11
  163. package/docs/actions.mdx +87 -0
  164. package/docs/agent-cheatsheet.md +102 -26
  165. package/docs/architecture.md +46 -35
  166. package/docs/benchmarks.md +76 -72
  167. package/docs/best-practices.mdx +211 -0
  168. package/docs/capabilities.mdx +387 -0
  169. package/docs/cli/affected.mdx +51 -0
  170. package/docs/cli/capsules.mdx +69 -0
  171. package/docs/cli/cloud.mdx +127 -0
  172. package/docs/cli/doctor.mdx +76 -0
  173. package/docs/cli/drive.mdx +55 -0
  174. package/docs/cli/feedback.mdx +67 -0
  175. package/docs/cli/gate.mdx +64 -0
  176. package/docs/cli/help.mdx +92 -0
  177. package/docs/cli/hunt.mdx +57 -0
  178. package/docs/cli/identify.mdx +62 -0
  179. package/docs/cli/init.mdx +94 -0
  180. package/docs/cli/kill.mdx +91 -0
  181. package/docs/cli/license.mdx +48 -0
  182. package/docs/cli/mcp.mdx +61 -0
  183. package/docs/cli/open.mdx +68 -0
  184. package/docs/cli/restart.mdx +60 -0
  185. package/docs/cli/rollback.mdx +35 -0
  186. package/docs/cli/serve.mdx +62 -0
  187. package/docs/cli/status.mdx +64 -0
  188. package/docs/cli/stop.mdx +56 -0
  189. package/docs/cli/telemetry.mdx +57 -0
  190. package/docs/cli/update.mdx +52 -0
  191. package/docs/cli/verify.mdx +57 -0
  192. package/docs/cli/version.mdx +39 -0
  193. package/docs/cli/watch.mdx +55 -0
  194. package/docs/cli.mdx +219 -0
  195. package/docs/debugging.md +13 -9
  196. package/docs/deploy-checks.md +15 -11
  197. package/docs/desktop-apps.md +49 -40
  198. package/docs/desktop.mdx +188 -0
  199. package/docs/docs.json +363 -0
  200. package/docs/enterprise.md +20 -16
  201. package/docs/faq.mdx +88 -0
  202. package/docs/fixtures.md +11 -7
  203. package/docs/flows.md +26 -22
  204. package/docs/for-agents.md +71 -0
  205. package/docs/frameworks.mdx +199 -0
  206. package/docs/gate-plan.md +45 -41
  207. package/docs/gates.md +23 -19
  208. package/docs/getting-started.md +76 -65
  209. package/docs/human-control.md +22 -18
  210. package/docs/index.mdx +101 -0
  211. package/docs/install-agentic.mdx +215 -0
  212. package/docs/install-manual.mdx +194 -0
  213. package/docs/instrumentation.mdx +232 -0
  214. package/docs/integration-patterns.md +38 -34
  215. package/docs/local-registry.md +16 -12
  216. package/docs/multi-agent-testing.md +16 -12
  217. package/docs/packages/babel-plugin.mdx +70 -0
  218. package/docs/packages/browser.mdx +111 -0
  219. package/docs/packages/core.mdx +106 -0
  220. package/docs/packages/electron.mdx +87 -0
  221. package/docs/packages/eslint-plugin.mdx +84 -0
  222. package/docs/packages/next.mdx +91 -0
  223. package/docs/packages/react.mdx +91 -0
  224. package/docs/packages/server.mdx +98 -0
  225. package/docs/packages/tauri.mdx +63 -0
  226. package/docs/packages/test.mdx +108 -0
  227. package/docs/packages/vite-plugin.mdx +96 -0
  228. package/docs/packages.mdx +217 -0
  229. package/docs/platform-integration.md +33 -26
  230. package/docs/predicates.mdx +179 -0
  231. package/docs/quickstart.mdx +211 -0
  232. package/docs/recipes.mdx +242 -0
  233. package/docs/skill-file.mdx +105 -0
  234. package/docs/state-management.mdx +197 -0
  235. package/docs/style.css +14 -0
  236. package/docs/system-map.md +47 -32
  237. package/docs/telemetry-contract.md +101 -46
  238. package/docs/telemetry-events.mdx +430 -0
  239. package/docs/telemetry.md +63 -50
  240. package/docs/testing.md +10 -6
  241. package/docs/token-efficiency.md +30 -26
  242. package/docs/tools-act-and-wait.mdx +123 -0
  243. package/docs/tools-act-sequence.mdx +120 -0
  244. package/docs/tools-act.mdx +82 -0
  245. package/docs/tools-assert.mdx +70 -0
  246. package/docs/tools-console.mdx +59 -0
  247. package/docs/tools-inspect.mdx +85 -0
  248. package/docs/tools-navigate.mdx +63 -0
  249. package/docs/tools-network.mdx +86 -0
  250. package/docs/tools-observe.mdx +89 -0
  251. package/docs/tools-overview.mdx +157 -0
  252. package/docs/tools-query.mdx +82 -0
  253. package/docs/tools-session-and-feedback.mdx +91 -0
  254. package/docs/tools-sessions.mdx +65 -0
  255. package/docs/tools-snapshot.mdx +75 -0
  256. package/docs/tools-state.mdx +81 -0
  257. package/docs/tools-tools-and-run.mdx +125 -0
  258. package/docs/tools-wait-for.mdx +66 -0
  259. package/docs/troubleshooting.mdx +193 -0
  260. package/docs/usage.md +175 -153
  261. package/docs/vs-chrome-devtools-mcp.mdx +82 -0
  262. package/docs/vs-playwright-mcp.mdx +85 -0
  263. package/docs/vs-screenshots.mdx +96 -0
  264. package/docs/what-is-reticle.mdx +63 -0
  265. package/docs/why-reticle.mdx +107 -0
  266. package/package.json +2 -2
  267. package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
  268. package/docs/matrix/2.5.0/codex-machine.json +0 -20
  269. package/docs/matrix/2.5.0/cursor-machine.json +0 -22
  270. package/docs/matrix/2.5.0/gemini-machine.json +0 -22
  271. package/docs/matrix/2.5.0/opencode-machine.json +0 -22
  272. package/docs/matrix/2.5.0/vscode-machine.json +0 -22
  273. package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
  274. package/docs/matrix/MATRIX.md +0 -26
  275. package/docs/matrix/README.md +0 -59
  276. package/docs/mint.json +0 -61
@@ -1,4 +1,4 @@
1
- import { ElementQuerySchema, ElementState, EventType, PredicateKind, StreamDirection, isDevToolingUrl, } from '@reticlehq/core';
1
+ import { ElementQuerySchema, ElementState, EventType, PredicateKind, QueryBy, StreamDirection, isDevToolingUrl, } from '@reticlehq/core';
2
2
  import { z } from 'zod';
3
3
  import { describeObserved } from './observed-in-window.js';
4
4
  /**
@@ -9,6 +9,11 @@ import { describeObserved } from './observed-in-window.js';
9
9
  * the only key supplied left a predicate that asserts nothing and passes on anything.
10
10
  */
11
11
  const PREDICATE_ALIASES = {
12
+ // `text`/`value` on a `text` predicate: the kind is called "text", so `text:` is the first thing
13
+ // anyone writes for it, and `value` follows from the element query's own `value` field. Both were
14
+ // hard rejections, and a rejected predicate produces NO verdict at all — the drive ends with
15
+ // nothing rather than with a failure, which is the worst outcome of the three.
16
+ [PredicateKind.TEXT]: { text: 'contains', value: 'contains' },
12
17
  // `urlContains`/`url` reported from the field: an agent that had just written
13
18
  // `net { urlContains }` applied the same word to `route`, which spells it `contains`, and got
14
19
  // `unrecognized_keys` with no list of what would have worked. The parallel it assumed is a fair
@@ -19,111 +24,265 @@ const PREDICATE_ALIASES = {
19
24
  [PredicateKind.NET]: { url: 'urlContains' },
20
25
  [PredicateKind.SIGNAL]: { data: 'dataMatches' },
21
26
  };
27
+ /**
28
+ * Element-query fields an agent writes FLAT on an `element` predicate instead of nested under
29
+ * `query`. `reticle_query` takes exactly these at the top level, so an agent that has just located
30
+ * something writes the same words again when it asserts on it — and got `query: Required` plus an
31
+ * `unknown field` list for its trouble. Lifting them is unambiguous: they have no other meaning on
32
+ * this kind.
33
+ */
34
+ const ELEMENT_QUERY_FIELDS = [
35
+ 'by',
36
+ 'value',
37
+ 'role',
38
+ 'name',
39
+ 'text',
40
+ 'label',
41
+ 'placeholder',
42
+ 'testid',
43
+ 'alt',
44
+ 'component',
45
+ ];
46
+ /**
47
+ * The locator fields the browser actually CONSUMES for a given query — mirrors the precedence in
48
+ * `findIn` (packages/browser/src/dom/query.ts).
49
+ *
50
+ * An element query is not a conjunction. It is a first-match dispatch: `by`+`value` wins, then the
51
+ * component/source anchor, then `role` (which alone also consumes `name`), then the first of
52
+ * text/label/placeholder/testid/alt that is present. Every OTHER field the caller wrote is dropped on
53
+ * the floor, silently, and the match reported as if the whole query had been honoured.
54
+ *
55
+ * Duplicated here on purpose. The alternative is to send the question to the browser, and the browser
56
+ * cannot answer it: by the time a match comes back, the fields it ignored are indistinguishable from
57
+ * the fields it used.
58
+ */
59
+ function usedQueryFields(query) {
60
+ const used = new Set();
61
+ if (query.by !== undefined && query.value !== undefined) {
62
+ used.add('by').add('value');
63
+ if (QueryBy.ROLE === query.by)
64
+ used.add('name');
65
+ if (QueryBy.COMPONENT === query.by)
66
+ used.add('component');
67
+ return used;
68
+ }
69
+ if (query.component !== undefined || query.source !== undefined)
70
+ return used.add('component');
71
+ if (query.role !== undefined)
72
+ return used.add('role').add('name');
73
+ for (const field of ['text', 'label', 'placeholder', 'testid', 'alt']) {
74
+ if (query[field] !== undefined)
75
+ return used.add(field);
76
+ }
77
+ return used;
78
+ }
79
+ /**
80
+ * How a dropped field is checked back on the server, against the descriptor the match returned.
81
+ *
82
+ * `value` is the field this exists for: `{ role: "textbox", name: "GST amount", value: "274.58" }`
83
+ * read as a locator has no `by`, so the value half was discarded and the predicate collapsed to "a
84
+ * textbox named GST amount exists" — trivially true against an EMPTY field. Comparison is TRIMMED and
85
+ * exact: an input's value is a value, not prose, and a trailing space in either the app or the
86
+ * predicate is not a finding anybody wants. `""` asserts the field is empty, which describe() reports
87
+ * by omitting the field entirely.
88
+ *
89
+ * `role`/`name`/`value` compare against exactly what `reticle_query` REPORTS, so the words an agent
90
+ * copies out of a query result are the words that match here. `text` is a substring match, matching
91
+ * Testing Library's `exact: false`, and falls back to the name because describe() omits `text` when it
92
+ * equals the accessible name.
93
+ */
94
+ const RESIDUAL_CHECKS = {
95
+ value: (element, want) => (element.value ?? '').trim() === want.trim(),
96
+ role: (element, want) => element.role === want,
97
+ name: (element, want) => element.name.trim() === want.trim(),
98
+ text: (element, want) => (element.text ?? element.name).includes(want),
99
+ };
100
+ /**
101
+ * Split a query's dropped fields into the ones the server can still enforce and the ones it cannot.
102
+ *
103
+ * The alternative — refuse every dropped field — breaks calls that work today and that our own
104
+ * cheatsheet advertises (`{ role: "button", text: "Save" }`), and breaks them into no verdict at all.
105
+ * Enforcing what we can and refusing only the rest keeps those calls working AS WRITTEN, which is the
106
+ * outcome the caller was already assuming.
107
+ */
108
+ export function residualQueryChecks(query) {
109
+ const used = usedQueryFields(query);
110
+ const checks = [];
111
+ const unusable = [];
112
+ for (const field of ELEMENT_QUERY_FIELDS) {
113
+ const want = query[field];
114
+ if (want === undefined || used.has(field))
115
+ continue;
116
+ if ('string' === typeof want && RESIDUAL_CHECKS[field] !== undefined)
117
+ checks.push([field, want]);
118
+ else
119
+ unusable.push(field);
120
+ }
121
+ return { checks, unusable };
122
+ }
123
+ /** Does this element satisfy every field the locator dropped? */
124
+ export function satisfiesResiduals(element, checks) {
125
+ return checks.every(([field, want]) => true === RESIDUAL_CHECKS[field]?.(element, want));
126
+ }
127
+ /** How the element's own reading of a dropped field should be REPORTED back on a failure. */
128
+ export function describeResidual(element, field) {
129
+ const reading = 'value' === field
130
+ ? (element.value ?? '')
131
+ : 'text' === field
132
+ ? (element.text ?? element.name)
133
+ : 'role' === field
134
+ ? element.role
135
+ : element.name;
136
+ return `${element.role} "${element.name}" ${field}=${JSON.stringify(reading)}`;
137
+ }
22
138
  /** Rename known aliases before parse; an explicit canonical key always wins. */
23
139
  function applyPredicateAliases(input) {
24
140
  if (typeof input !== 'object' || null === input || Array.isArray(input))
25
141
  return input;
26
142
  const obj = input;
27
- const aliases = PREDICATE_ALIASES['string' === typeof obj['kind'] ? obj['kind'] : ''];
28
- if (aliases === undefined)
29
- return input;
143
+ const kind = 'string' === typeof obj['kind'] ? obj['kind'] : '';
144
+ const aliases = PREDICATE_ALIASES[kind];
145
+ let out = obj;
146
+ if (aliases !== undefined) {
147
+ out = { ...obj };
148
+ for (const [from, to] of Object.entries(aliases)) {
149
+ if (out[from] === undefined)
150
+ continue;
151
+ if (out[to] === undefined)
152
+ out[to] = out[from];
153
+ delete out[from];
154
+ }
155
+ }
156
+ return PredicateKind.ELEMENT === kind ? liftElementQuery(out) : out;
157
+ }
158
+ /**
159
+ * Fold flat query fields into `query`. An explicit `query` wins outright — a caller that supplied
160
+ * both told us which one it meant, and merging the two would invent a locator neither side wrote.
161
+ */
162
+ function liftElementQuery(obj) {
163
+ const loose = ELEMENT_QUERY_FIELDS.filter((field) => obj[field] !== undefined);
164
+ if (0 === loose.length)
165
+ return obj;
30
166
  const out = { ...obj };
31
- for (const [from, to] of Object.entries(aliases)) {
32
- if (out[from] === undefined)
33
- continue;
34
- if (out[to] === undefined)
35
- out[to] = out[from];
36
- delete out[from];
167
+ const query = {};
168
+ for (const field of loose) {
169
+ query[field] = out[field];
170
+ delete out[field];
37
171
  }
172
+ if (out['query'] === undefined)
173
+ out['query'] = query;
38
174
  return out;
39
175
  }
40
176
  /**
41
177
  * Strict on every branch. A key that is nobody's spelling is now a schema error naming it, instead of
42
178
  * a stripped field and a green — see predicate-strict.test.ts for the MCP session that found this.
179
+ *
180
+ * Built on demand rather than at module scope: the `allOf`/`anyOf`/`not` branches reference
181
+ * `PredicateSchema` itself, which does not exist yet while this module is initialising. Calling it
182
+ * after init is what makes the union introspectable — see `predicateFieldsFor`.
43
183
  */
44
- export const PredicateSchema = z.lazy(() => z.preprocess(applyPredicateAliases, z.discriminatedUnion('kind', [
45
- z
46
- .object({
47
- kind: z.literal(PredicateKind.ELEMENT),
48
- query: ElementQuerySchema,
49
- state: z.nativeEnum(ElementState).optional(),
50
- absent: z.boolean().optional(),
51
- })
52
- .strict(),
53
- z
54
- .object({
55
- kind: z.literal(PredicateKind.TEXT),
56
- contains: z.string(),
57
- visible: z.boolean().optional(),
58
- absent: z.boolean().optional(),
59
- })
60
- .strict(),
61
- z
62
- .object({
63
- kind: z.literal(PredicateKind.NET),
64
- method: z.string().optional(),
65
- urlContains: z.string().optional(),
66
- status: z.number().optional(),
67
- ok: z.boolean().optional(),
68
- since: z.number().optional(),
69
- count: z.number().int().nonnegative().optional(),
70
- })
71
- .strict(),
72
- z
73
- .object({
74
- kind: z.literal(PredicateKind.ROUTE),
75
- pathname: z.string().optional(),
76
- contains: z.string().optional(),
77
- since: z.number().optional(),
78
- })
79
- .strict(),
80
- z
81
- .object({
82
- kind: z.literal(PredicateKind.CONSOLE),
83
- level: z.string().optional(),
84
- absent: z.boolean().optional(),
85
- since: z.number().optional(),
86
- })
87
- .strict(),
88
- z
89
- .object({
90
- kind: z.literal(PredicateKind.ANIMATION),
91
- name: z.string().optional(),
92
- target: z.string().optional(),
93
- completed: z.boolean().optional(),
94
- since: z.number().optional(),
95
- })
96
- .strict(),
97
- z
98
- .object({
99
- kind: z.literal(PredicateKind.SIGNAL),
100
- name: z.string().optional(),
101
- dataMatches: z.record(z.unknown()).optional(),
102
- since: z.number().optional(),
103
- })
104
- .strict(),
105
- z
106
- .object({
107
- kind: z.literal(PredicateKind.STATE),
108
- store: z.string().optional(),
109
- path: z.string(),
110
- equals: z.unknown().optional(),
111
- })
112
- .strict(),
113
- z
114
- .object({
115
- kind: z.literal(PredicateKind.SETTLED),
116
- quietMs: z.number().positive().optional(),
117
- })
118
- .strict(),
119
- z
120
- .object({ kind: z.literal(PredicateKind.ALL_OF), predicates: z.array(PredicateSchema) })
121
- .strict(),
122
- z
123
- .object({ kind: z.literal(PredicateKind.ANY_OF), predicates: z.array(PredicateSchema) })
124
- .strict(),
125
- z.object({ kind: z.literal(PredicateKind.NOT), predicate: PredicateSchema }).strict(),
126
- ])));
184
+ function predicateUnion() {
185
+ return z.discriminatedUnion('kind', [
186
+ z
187
+ .object({
188
+ kind: z.literal(PredicateKind.ELEMENT),
189
+ query: ElementQuerySchema,
190
+ state: z.nativeEnum(ElementState).optional(),
191
+ absent: z.boolean().optional(),
192
+ })
193
+ .strict(),
194
+ z
195
+ .object({
196
+ kind: z.literal(PredicateKind.TEXT),
197
+ contains: z.string(),
198
+ visible: z.boolean().optional(),
199
+ absent: z.boolean().optional(),
200
+ })
201
+ .strict(),
202
+ z
203
+ .object({
204
+ kind: z.literal(PredicateKind.NET),
205
+ method: z.string().optional(),
206
+ urlContains: z.string().optional(),
207
+ status: z.number().optional(),
208
+ ok: z.boolean().optional(),
209
+ since: z.number().optional(),
210
+ count: z.number().int().nonnegative().optional(),
211
+ })
212
+ .strict(),
213
+ z
214
+ .object({
215
+ kind: z.literal(PredicateKind.ROUTE),
216
+ pathname: z.string().optional(),
217
+ contains: z.string().optional(),
218
+ since: z.number().optional(),
219
+ })
220
+ .strict(),
221
+ z
222
+ .object({
223
+ kind: z.literal(PredicateKind.CONSOLE),
224
+ level: z.string().optional(),
225
+ absent: z.boolean().optional(),
226
+ since: z.number().optional(),
227
+ })
228
+ .strict(),
229
+ z
230
+ .object({
231
+ kind: z.literal(PredicateKind.ANIMATION),
232
+ name: z.string().optional(),
233
+ target: z.string().optional(),
234
+ completed: z.boolean().optional(),
235
+ since: z.number().optional(),
236
+ })
237
+ .strict(),
238
+ z
239
+ .object({
240
+ kind: z.literal(PredicateKind.SIGNAL),
241
+ name: z.string().optional(),
242
+ dataMatches: z.record(z.unknown()).optional(),
243
+ since: z.number().optional(),
244
+ })
245
+ .strict(),
246
+ z
247
+ .object({
248
+ kind: z.literal(PredicateKind.STATE),
249
+ store: z.string().optional(),
250
+ path: z.string(),
251
+ equals: z.unknown().optional(),
252
+ })
253
+ .strict(),
254
+ z
255
+ .object({
256
+ kind: z.literal(PredicateKind.SETTLED),
257
+ quietMs: z.number().positive().optional(),
258
+ })
259
+ .strict(),
260
+ z
261
+ .object({ kind: z.literal(PredicateKind.ALL_OF), predicates: z.array(PredicateSchema) })
262
+ .strict(),
263
+ z
264
+ .object({ kind: z.literal(PredicateKind.ANY_OF), predicates: z.array(PredicateSchema) })
265
+ .strict(),
266
+ z.object({ kind: z.literal(PredicateKind.NOT), predicate: PredicateSchema }).strict(),
267
+ ]);
268
+ }
269
+ export const PredicateSchema = z.lazy(() => z.preprocess(applyPredicateAliases, predicateUnion()));
270
+ /**
271
+ * The fields a given predicate kind accepts, read off the schema itself.
272
+ *
273
+ * Derived rather than listed so the two can never disagree: a rejection message that names a stale
274
+ * field set is worse than one that names none, because the agent trusts it and retries into the same
275
+ * wall. Empty for a kind that is not in the union.
276
+ */
277
+ export function predicateFieldsFor(kind) {
278
+ for (const option of predicateUnion().options) {
279
+ const literal = option.shape['kind'];
280
+ if (literal instanceof z.ZodLiteral && literal.value === kind) {
281
+ return Object.keys(option.shape).filter((field) => 'kind' !== field);
282
+ }
283
+ }
284
+ return [];
285
+ }
127
286
  function str(value) {
128
287
  return 'string' === typeof value ? value : undefined;
129
288
  }
@@ -189,7 +348,37 @@ export function matchValue(got, want) {
189
348
  }
190
349
  return true;
191
350
  }
192
- return got === want;
351
+ return structurallyEqual(got, want);
352
+ }
353
+ /**
354
+ * Value equality for the leaf comparison, because `===` could never match a literal.
355
+ *
356
+ * The expected side of a predicate is parsed out of the agent's JSON, so it is a fresh object every
357
+ * time — reference equality made `equals: ["a", "b"]` false against a store holding exactly
358
+ * `["a", "b"]`, and there was no value of the app's state that could have made it true. The mirror of
359
+ * a false green: an assertion nobody could satisfy, on the commonest thing there is to assert about.
360
+ *
361
+ * Deliberately strict about shape. An array is not an object, an extra key is a difference, and order
362
+ * is part of a list's value — `equals` means equals; `dataMatches` is the field-by-field one.
363
+ */
364
+ function structurallyEqual(got, want) {
365
+ if (got === want)
366
+ return true;
367
+ if (null === got || null === want)
368
+ return false;
369
+ if ('object' !== typeof got || 'object' !== typeof want)
370
+ return false;
371
+ if (Array.isArray(got) !== Array.isArray(want))
372
+ return false;
373
+ if (Array.isArray(got) && Array.isArray(want)) {
374
+ return got.length === want.length && got.every((v, i) => structurallyEqual(v, want[i]));
375
+ }
376
+ const a = got;
377
+ const b = want;
378
+ const keys = Object.keys(a);
379
+ if (keys.length !== Object.keys(b).length)
380
+ return false;
381
+ return keys.every((k) => k in b && structurallyEqual(a[k], b[k]));
193
382
  }
194
383
  /** Shallow JSON pattern match: each key in `pattern` must match (see matchValue). */
195
384
  function dataMatches(actual, pattern) {
@@ -419,7 +608,7 @@ export function evalSignal(events, p) {
419
608
  ? `signal '${p.name ?? '(any)'}' fired ${String(sameName.length)}x, payload: ${JSON.stringify(sameName[0])}`
420
609
  : // Name what DID fire: a typo'd signal name and a genuinely dead action produce the same
421
610
  // sentence otherwise, and the agent cannot tell them apart. See observed-in-window.ts.
422
- `signal '${p.name ?? '(any)'}' never fired — ${describeObserved('signals', events.filter((e) => e.type === EventType.SIGNAL).map((e) => str(e.data['name']) ?? ''))}`,
611
+ `signal '${p.name ?? '(any)'}' never fired; ${describeObserved('signals', events.filter((e) => e.type === EventType.SIGNAL).map((e) => str(e.data['name']) ?? ''))}`,
423
612
  expected: p.dataMatches === undefined
424
613
  ? `signal '${p.name ?? '(any)'}' to fire`
425
614
  : `signal '${p.name ?? '(any)'}' with payload matching ${JSON.stringify(p.dataMatches)}`,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * One readable sentence when a predicate does not parse — never the zod array.
3
3
  *
4
- * Measured 2026-08-10: **9 of 58 tool errors that day were a serialized zod issue array**, all on
4
+ * In the field a meaningful share of all tool errors were **a serialized zod issue array**, all on
5
5
  * `reticle_act_and_wait`, `reticle_wait_for` and `reticle_assert` — the three tools that produced
6
6
  * every action-derived finding in the dataset. The least readable error we emit was landing on the
7
7
  * highest-value path, and an agent had to `JSON.parse` an error string to learn which field it got
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * One readable sentence when a predicate does not parse — never the zod array.
3
3
  *
4
- * Measured 2026-08-10: **9 of 58 tool errors that day were a serialized zod issue array**, all on
4
+ * In the field a meaningful share of all tool errors were **a serialized zod issue array**, all on
5
5
  * `reticle_act_and_wait`, `reticle_wait_for` and `reticle_assert` — the three tools that produced
6
6
  * every action-derived finding in the dataset. The least readable error we emit was landing on the
7
7
  * highest-value path, and an agent had to `JSON.parse` an error string to learn which field it got
@@ -13,7 +13,8 @@
13
13
  * show a valid call.
14
14
  */
15
15
  import { z } from 'zod';
16
- import { PredicateSchema } from './predicate-eval.js';
16
+ import { PredicateKind } from '@reticlehq/core';
17
+ import { PredicateSchema, predicateFieldsFor } from './predicate-eval.js';
17
18
  /** A valid call, short enough to sit inside an error without becoming the error. */
18
19
  const EXAMPLE = '{ kind: "signal", name: "todos:loaded" }';
19
20
  /** `path: ["net","urlContains"]` → `net.urlContains`; an empty path means the object itself. */
@@ -49,6 +50,19 @@ export function parsePredicate(input) {
49
50
  // back is how the zod array became unreadable in the first place.
50
51
  const issues = parsed.error.issues.slice(0, 3).map(describeIssue).join('; ');
51
52
  throw new Error(`that predicate did not parse (kind "${kind}"): ${issues}. Nothing ran — the predicate was ` +
52
- `not evaluated, so no verdict was produced. Call reticle_tools for the fields this kind ` +
53
- `accepts. A valid call looks like: ${EXAMPLE}`);
53
+ `not evaluated, so no verdict was produced. ${accepted(kind)} ` +
54
+ `A valid call looks like: ${EXAMPLE}`);
55
+ }
56
+ /**
57
+ * What WOULD have worked, in the same breath as what did not.
58
+ *
59
+ * Telling an agent only which field is wrong leaves it guessing again, and each guess costs another
60
+ * round trip on the one call path that produces verdicts. Naming the accepted fields — or, when the
61
+ * kind itself is the mistake, the accepted kinds — makes the retry informed instead.
62
+ */
63
+ function accepted(kind) {
64
+ const fields = predicateFieldsFor(kind);
65
+ if (0 < fields.length)
66
+ return `${kind} accepts: ${fields.join(', ')}.`;
67
+ return `"${kind}" is not a predicate kind — use one of: ${Object.values(PredicateKind).join(', ')}.`;
54
68
  }