@reticlehq/server 2.7.0 → 2.9.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 (357) hide show
  1. package/README.md +14 -5
  2. package/SKILL.md +130 -778
  3. package/dist/bridge/bridge.d.ts +23 -0
  4. package/dist/bridge/bridge.js +80 -2
  5. package/dist/bridge/bridge.test-harness.d.ts +5 -0
  6. package/dist/bridge/bridge.test-harness.js +6 -4
  7. package/dist/capsule/causal-summary.d.ts +36 -1
  8. package/dist/capsule/causal-summary.js +33 -7
  9. package/dist/capsule/divergence.js +7 -1
  10. package/dist/cli/chromium-hint.d.ts +33 -5
  11. package/dist/cli/chromium-hint.js +82 -12
  12. package/dist/cli/cli-doctor.js +65 -34
  13. package/dist/cli/cli-flow-commands.js +8 -8
  14. package/dist/cli/cli-kill.d.ts +58 -0
  15. package/dist/cli/cli-kill.js +162 -0
  16. package/dist/cli/cli-parse.d.ts +16 -1
  17. package/dist/cli/cli-parse.js +32 -9
  18. package/dist/cli/cli-port.d.ts +32 -0
  19. package/dist/cli/cli-port.js +82 -35
  20. package/dist/cli/cli-verify.d.ts +21 -0
  21. package/dist/cli/cli-verify.js +47 -3
  22. package/dist/cli/config-discovery.d.ts +35 -0
  23. package/dist/cli/config-discovery.js +163 -0
  24. package/dist/cli/doctor-daemon-line.js +4 -1
  25. package/dist/cli/doctor-project-line.d.ts +37 -0
  26. package/dist/cli/doctor-project-line.js +35 -0
  27. package/dist/cli/doctor-rows.d.ts +51 -0
  28. package/dist/cli/doctor-rows.js +54 -0
  29. package/dist/cli/doctor-sessions-line.d.ts +12 -0
  30. package/dist/cli/doctor-sessions-line.js +69 -0
  31. package/dist/cli/drive-attach.d.ts +99 -0
  32. package/dist/cli/drive-attach.js +203 -0
  33. package/dist/cli/drive-command.d.ts +13 -0
  34. package/dist/cli/drive-command.js +103 -0
  35. package/dist/cli/open-note.d.ts +20 -0
  36. package/dist/cli/open-note.js +37 -0
  37. package/dist/cli/port-holder.d.ts +10 -0
  38. package/dist/cli/port-holder.js +26 -2
  39. package/dist/cli/status-next-action.d.ts +37 -0
  40. package/dist/cli/status-next-action.js +61 -0
  41. package/dist/cli.js +119 -39
  42. package/dist/crawl/crawl-tools.js +12 -2
  43. package/dist/crawl/crawl.d.ts +13 -0
  44. package/dist/crawl/crawl.js +50 -2
  45. package/dist/events/contradictions.d.ts +46 -0
  46. package/dist/events/contradictions.js +35 -22
  47. package/dist/events/declared.d.ts +45 -0
  48. package/dist/events/declared.js +73 -0
  49. package/dist/events/echo-mismatch.d.ts +8 -2
  50. package/dist/events/echo-mismatch.js +33 -5
  51. package/dist/events/event-filters.d.ts +1 -1
  52. package/dist/events/event-filters.js +7 -4
  53. package/dist/events/predicate-eval.d.ts +17 -72
  54. package/dist/events/predicate-eval.js +117 -236
  55. package/dist/events/predicate-parse.js +31 -3
  56. package/dist/events/predicate-route.d.ts +32 -0
  57. package/dist/events/predicate-route.js +102 -0
  58. package/dist/events/predicate-schema.d.ts +99 -0
  59. package/dist/events/predicate-schema.js +309 -0
  60. package/dist/events/predicate.d.ts +8 -0
  61. package/dist/events/predicate.js +296 -22
  62. package/dist/flows/annotate-tools.js +3 -0
  63. package/dist/flows/annotate.js +18 -4
  64. package/dist/flows/coverage.d.ts +38 -5
  65. package/dist/flows/coverage.js +30 -4
  66. package/dist/flows/decision.d.ts +15 -0
  67. package/dist/flows/decision.js +1 -1
  68. package/dist/flows/flow-classify.js +1 -1
  69. package/dist/flows/flow-replay-run.js +8 -1
  70. package/dist/flows/flow-tools.js +21 -5
  71. package/dist/flows/heal-run.js +1 -1
  72. package/dist/flows/replay.js +3 -0
  73. package/dist/honesty/accepted-write.d.ts +3 -0
  74. package/dist/honesty/accepted-write.js +8 -3
  75. package/dist/honesty/already-true.js +15 -0
  76. package/dist/honesty/blind-spots.js +10 -2
  77. package/dist/honesty/honesty.d.ts +11 -0
  78. package/dist/honesty/honesty.js +1 -0
  79. package/dist/honesty/uncaptured-bodies.d.ts +4 -2
  80. package/dist/honesty/uncaptured-bodies.js +9 -2
  81. package/dist/honesty/unread-outcome.d.ts +8 -0
  82. package/dist/honesty/unread-outcome.js +36 -9
  83. package/dist/honesty/unsettled.d.ts +49 -0
  84. package/dist/honesty/unsettled.js +82 -0
  85. package/dist/honesty/verified.d.ts +36 -3
  86. package/dist/honesty/verified.js +54 -11
  87. package/dist/http-server.d.ts +19 -0
  88. package/dist/http-server.js +202 -4
  89. package/dist/impact/impact-recorder.d.ts +23 -0
  90. package/dist/impact/impact-recorder.js +76 -0
  91. package/dist/impact/impact-store.d.ts +47 -0
  92. package/dist/impact/impact-store.js +171 -0
  93. package/dist/index.d.ts +7 -0
  94. package/dist/index.js +47 -10
  95. package/dist/init/agent-rules.d.ts +36 -0
  96. package/dist/init/agent-rules.js +132 -22
  97. package/dist/init/astro-patch.d.ts +2 -1
  98. package/dist/init/astro-patch.js +38 -6
  99. package/dist/init/capabilities.d.ts +22 -0
  100. package/dist/init/capabilities.js +76 -0
  101. package/dist/init/confirm.d.ts +114 -0
  102. package/dist/init/confirm.js +165 -0
  103. package/dist/init/connect-steps.d.ts +2 -0
  104. package/dist/init/connect-steps.js +50 -0
  105. package/dist/init/cra.js +3 -2
  106. package/dist/init/csp-check.d.ts +38 -0
  107. package/dist/init/csp-check.js +92 -0
  108. package/dist/init/csp-doctor.d.ts +25 -0
  109. package/dist/init/csp-doctor.js +49 -0
  110. package/dist/init/cursor.js +6 -9
  111. package/dist/init/detect.d.ts +8 -0
  112. package/dist/init/detect.js +33 -3
  113. package/dist/init/mcp-clients.d.ts +1 -0
  114. package/dist/init/mcp-clients.js +51 -7
  115. package/dist/init/mcp.d.ts +15 -0
  116. package/dist/init/mcp.js +19 -0
  117. package/dist/init/non-js-project.js +29 -5
  118. package/dist/init/plan-framework.d.ts +10 -0
  119. package/dist/init/plan-framework.js +78 -16
  120. package/dist/init/plan.d.ts +47 -6
  121. package/dist/init/plan.js +228 -69
  122. package/dist/init/run.d.ts +24 -2
  123. package/dist/init/run.js +157 -45
  124. package/dist/init/slash-command.d.ts +1 -1
  125. package/dist/init/slash-command.js +54 -6
  126. package/dist/init/snippets.d.ts +45 -5
  127. package/dist/init/snippets.js +194 -42
  128. package/dist/init/vite-config.js +53 -5
  129. package/dist/input/network-mock-tools.d.ts +19 -0
  130. package/dist/input/network-mock-tools.js +37 -5
  131. package/dist/input/scroll-find.d.ts +6 -0
  132. package/dist/input/scroll-find.js +17 -2
  133. package/dist/input/scroll-tools.js +4 -0
  134. package/dist/journal/attach-journal.d.ts +9 -1
  135. package/dist/journal/attach-journal.js +13 -2
  136. package/dist/journal/journal-query.d.ts +13 -0
  137. package/dist/journal/journal-query.js +28 -3
  138. package/dist/journal/workspace-gitignore.d.ts +10 -0
  139. package/dist/journal/workspace-gitignore.js +59 -0
  140. package/dist/license/license.js +1 -1
  141. package/dist/mcp/mcp-proxy.d.ts +35 -30
  142. package/dist/mcp/mcp-proxy.js +306 -104
  143. package/dist/mcp/mcp.d.ts +10 -1
  144. package/dist/mcp/mcp.js +70 -34
  145. package/dist/mcp/proxy-handshake.d.ts +82 -1
  146. package/dist/mcp/proxy-handshake.js +109 -2
  147. package/dist/mcp/proxy-lifecycle.d.ts +11 -2
  148. package/dist/mcp/proxy-lifecycle.js +12 -3
  149. package/dist/mcp/proxy-log.d.ts +63 -0
  150. package/dist/mcp/proxy-log.js +132 -0
  151. package/dist/mcp/server-instructions.d.ts +17 -0
  152. package/dist/mcp/server-instructions.js +56 -0
  153. package/dist/mcp/shared-params.d.ts +27 -0
  154. package/dist/mcp/shared-params.js +36 -0
  155. package/dist/pool/browser-pool.d.ts +22 -2
  156. package/dist/pool/browser-pool.js +44 -4
  157. package/dist/project/learned-routes.d.ts +24 -0
  158. package/dist/project/learned-routes.js +63 -0
  159. package/dist/project/project-store.d.ts +6 -0
  160. package/dist/project/project-store.js +25 -1
  161. package/dist/project/reticle-dir.d.ts +2 -0
  162. package/dist/project/reticle-dir.js +1 -0
  163. package/dist/session/connection-memory.d.ts +59 -0
  164. package/dist/session/connection-memory.js +108 -0
  165. package/dist/session/dev-command.d.ts +26 -0
  166. package/dist/session/dev-command.js +89 -0
  167. package/dist/session/dev-server-probe.d.ts +68 -0
  168. package/dist/session/dev-server-probe.js +102 -7
  169. package/dist/session/last-act.d.ts +26 -0
  170. package/dist/session/last-act.js +35 -1
  171. package/dist/session/live-control-tools.js +42 -5
  172. package/dist/session/no-session-diagnosis.d.ts +67 -2
  173. package/dist/session/no-session-diagnosis.js +236 -27
  174. package/dist/session/no-session-next-action.d.ts +46 -0
  175. package/dist/session/no-session-next-action.js +85 -0
  176. package/dist/session/no-session-watch.d.ts +40 -1
  177. package/dist/session/no-session-watch.js +174 -8
  178. package/dist/session/pending-commands.d.ts +12 -0
  179. package/dist/session/pending-commands.js +13 -1
  180. package/dist/session/ref-provenance.d.ts +54 -0
  181. package/dist/session/ref-provenance.js +61 -0
  182. package/dist/session/session-lease.js +1 -1
  183. package/dist/session/session-manager.d.ts +4 -0
  184. package/dist/session/session-manager.js +35 -5
  185. package/dist/session/session.d.ts +10 -1
  186. package/dist/session/session.js +146 -5
  187. package/dist/telemetry/app-instrumented.d.ts +8 -0
  188. package/dist/telemetry/app-instrumented.js +10 -0
  189. package/dist/telemetry/automation-hint.d.ts +17 -0
  190. package/dist/telemetry/automation-hint.js +71 -0
  191. package/dist/telemetry/bug-found.d.ts +15 -17
  192. package/dist/telemetry/bug-found.js +52 -21
  193. package/dist/telemetry/cli-telemetry.js +7 -1
  194. package/dist/telemetry/daemon-telemetry.js +24 -0
  195. package/dist/telemetry/feedback.js +1 -1
  196. package/dist/telemetry/init-telemetry.js +9 -1
  197. package/dist/telemetry/install-source.d.ts +56 -0
  198. package/dist/telemetry/install-source.js +74 -0
  199. package/dist/telemetry/instrumentation-stall.d.ts +50 -0
  200. package/dist/telemetry/instrumentation-stall.js +90 -0
  201. package/dist/telemetry/run-telemetry.js +4 -1
  202. package/dist/telemetry/session-metrics.d.ts +8 -0
  203. package/dist/telemetry/session-metrics.js +15 -0
  204. package/dist/telemetry/telemetry.d.ts +7 -1
  205. package/dist/telemetry/telemetry.js +26 -2
  206. package/dist/telemetry/tool-refused.d.ts +19 -0
  207. package/dist/telemetry/tool-refused.js +55 -0
  208. package/dist/telemetry/verification-of.js +46 -7
  209. package/dist/temp-dir.d.ts +27 -0
  210. package/dist/temp-dir.js +46 -0
  211. package/dist/tools/act-tools.js +209 -28
  212. package/dist/tools/assert-grade.d.ts +1 -1
  213. package/dist/tools/assert-grade.js +27 -5
  214. package/dist/tools/assert-verdict.js +49 -7
  215. package/dist/tools/browser-tools.js +10 -0
  216. package/dist/tools/contract-tools.js +4 -0
  217. package/dist/tools/dynamic-tools.js +16 -7
  218. package/dist/tools/error-recovery.d.ts +10 -0
  219. package/dist/tools/error-recovery.js +73 -9
  220. package/dist/tools/feedback-invite.js +1 -1
  221. package/dist/tools/invoke-tool.js +159 -26
  222. package/dist/tools/lease-hint.d.ts +30 -8
  223. package/dist/tools/lease-hint.js +65 -14
  224. package/dist/tools/lease-tools.d.ts +40 -1
  225. package/dist/tools/lease-tools.js +181 -83
  226. package/dist/tools/navigate-result.js +1 -1
  227. package/dist/tools/observe-tools.js +10 -4
  228. package/dist/tools/observed-nothing.js +1 -1
  229. package/dist/tools/react-grace.d.ts +52 -0
  230. package/dist/tools/react-grace.js +90 -0
  231. package/dist/tools/read-tools.js +67 -5
  232. package/dist/tools/resolve-target.d.ts +33 -0
  233. package/dist/tools/resolve-target.js +53 -0
  234. package/dist/tools/settle-in-flight.d.ts +94 -0
  235. package/dist/tools/settle-in-flight.js +146 -0
  236. package/dist/tools/snapshot-delta.d.ts +10 -0
  237. package/dist/tools/snapshot-delta.js +194 -9
  238. package/dist/tools/tool-kit.d.ts +8 -0
  239. package/dist/tools/tool-surface.d.ts +60 -0
  240. package/dist/tools/tool-surface.js +86 -1
  241. package/dist/tools/tools.js +61 -8
  242. package/dist/tools/unadvertised-help.js +9 -6
  243. package/docs/README.md +55 -11
  244. package/docs/actions.mdx +120 -0
  245. package/docs/agent-cheatsheet.md +95 -29
  246. package/docs/architecture.md +52 -35
  247. package/docs/benchmarks.md +127 -75
  248. package/docs/best-practices.mdx +216 -0
  249. package/docs/capabilities.mdx +534 -0
  250. package/docs/cli/affected.mdx +53 -0
  251. package/docs/cli/capsules.mdx +70 -0
  252. package/docs/cli/cloud.mdx +153 -0
  253. package/docs/cli/doctor.mdx +109 -0
  254. package/docs/cli/drive.mdx +74 -0
  255. package/docs/cli/feedback.mdx +67 -0
  256. package/docs/cli/gate.mdx +64 -0
  257. package/docs/cli/help.mdx +94 -0
  258. package/docs/cli/hunt.mdx +73 -0
  259. package/docs/cli/identify.mdx +76 -0
  260. package/docs/cli/init.mdx +121 -0
  261. package/docs/cli/kill.mdx +91 -0
  262. package/docs/cli/license.mdx +48 -0
  263. package/docs/cli/mcp.mdx +61 -0
  264. package/docs/cli/open.mdx +74 -0
  265. package/docs/cli/restart.mdx +60 -0
  266. package/docs/cli/rollback.mdx +35 -0
  267. package/docs/cli/serve.mdx +69 -0
  268. package/docs/cli/status.mdx +88 -0
  269. package/docs/cli/stop.mdx +64 -0
  270. package/docs/cli/telemetry.mdx +57 -0
  271. package/docs/cli/update.mdx +52 -0
  272. package/docs/cli/verify.mdx +76 -0
  273. package/docs/cli/version.mdx +39 -0
  274. package/docs/cli/watch.mdx +62 -0
  275. package/docs/cli.mdx +241 -0
  276. package/docs/debugging.md +16 -10
  277. package/docs/deploy-checks.md +45 -12
  278. package/docs/desktop.mdx +384 -0
  279. package/docs/docs.json +364 -0
  280. package/docs/enterprise.md +22 -16
  281. package/docs/faq.mdx +102 -0
  282. package/docs/fixtures.md +13 -7
  283. package/docs/flows.md +29 -23
  284. package/docs/for-agents.md +71 -0
  285. package/docs/frameworks.mdx +238 -0
  286. package/docs/gate-plan.md +48 -42
  287. package/docs/gates.md +28 -22
  288. package/docs/getting-started.md +129 -77
  289. package/docs/human-control.md +24 -18
  290. package/docs/index.mdx +143 -0
  291. package/docs/install-agentic.mdx +224 -0
  292. package/docs/install-manual.mdx +196 -0
  293. package/docs/instrumentation.mdx +238 -0
  294. package/docs/integration-patterns.md +41 -35
  295. package/docs/local-registry.md +19 -13
  296. package/docs/multi-agent-testing.md +19 -13
  297. package/docs/packages/babel-plugin.mdx +79 -0
  298. package/docs/packages/browser.mdx +111 -0
  299. package/docs/packages/core.mdx +116 -0
  300. package/docs/packages/electron.mdx +85 -0
  301. package/docs/packages/eslint-plugin.mdx +86 -0
  302. package/docs/packages/next.mdx +89 -0
  303. package/docs/packages/react.mdx +91 -0
  304. package/docs/packages/server.mdx +98 -0
  305. package/docs/packages/tauri.mdx +63 -0
  306. package/docs/packages/test.mdx +119 -0
  307. package/docs/packages/vite-plugin.mdx +94 -0
  308. package/docs/packages.mdx +218 -0
  309. package/docs/platform-integration.md +35 -26
  310. package/docs/predicates.mdx +248 -0
  311. package/docs/quickstart.mdx +257 -0
  312. package/docs/recipes.mdx +282 -0
  313. package/docs/skill-file.mdx +107 -0
  314. package/docs/state-management.mdx +199 -0
  315. package/docs/style.css +14 -0
  316. package/docs/system-map.md +50 -33
  317. package/docs/telemetry-contract.md +106 -52
  318. package/docs/telemetry-events.mdx +475 -0
  319. package/docs/telemetry.md +63 -55
  320. package/docs/testing.md +44 -6
  321. package/docs/token-efficiency.md +37 -27
  322. package/docs/tools-act-and-wait.mdx +173 -0
  323. package/docs/tools-act-sequence.mdx +137 -0
  324. package/docs/tools-act.mdx +95 -0
  325. package/docs/tools-assert.mdx +129 -0
  326. package/docs/tools-console.mdx +63 -0
  327. package/docs/tools-inspect.mdx +88 -0
  328. package/docs/tools-navigate.mdx +99 -0
  329. package/docs/tools-network.mdx +119 -0
  330. package/docs/tools-observe.mdx +145 -0
  331. package/docs/tools-overview.mdx +160 -0
  332. package/docs/tools-query.mdx +126 -0
  333. package/docs/tools-session-and-feedback.mdx +112 -0
  334. package/docs/tools-sessions.mdx +94 -0
  335. package/docs/tools-snapshot.mdx +129 -0
  336. package/docs/tools-state.mdx +104 -0
  337. package/docs/tools-tools-and-run.mdx +156 -0
  338. package/docs/tools-wait-for.mdx +85 -0
  339. package/docs/troubleshooting.mdx +263 -0
  340. package/docs/usage.md +209 -165
  341. package/docs/vs-chrome-devtools-mcp.mdx +123 -0
  342. package/docs/vs-playwright-mcp.mdx +125 -0
  343. package/docs/vs-screenshots.mdx +137 -0
  344. package/docs/what-is-reticle.mdx +63 -0
  345. package/docs/why-reticle.mdx +109 -0
  346. package/package.json +5 -5
  347. package/docs/desktop-apps.md +0 -256
  348. package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
  349. package/docs/matrix/2.5.0/codex-machine.json +0 -20
  350. package/docs/matrix/2.5.0/cursor-machine.json +0 -22
  351. package/docs/matrix/2.5.0/gemini-machine.json +0 -22
  352. package/docs/matrix/2.5.0/opencode-machine.json +0 -22
  353. package/docs/matrix/2.5.0/vscode-machine.json +0 -22
  354. package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
  355. package/docs/matrix/MATRIX.md +0 -26
  356. package/docs/matrix/README.md +0 -59
  357. package/docs/mint.json +0 -61
@@ -4,6 +4,7 @@ import { findBodyFailures } from './body-failures.js';
4
4
  import { findEchoMismatches } from './echo-mismatch.js';
5
5
  import { findUnitMismatches } from './unit-mismatch.js';
6
6
  import { asNumber, asString } from '../tools/tools-helpers.js';
7
+ import { matchesDeclaredFailure } from './declared.js';
7
8
  function netCall(e) {
8
9
  const status = asNumber(e.data['status']);
9
10
  return {
@@ -171,7 +172,7 @@ export function findContradictions(allEvents, options = {}) {
171
172
  // Needs body capture; silent without it, which is why the assert path also declares when bodies
172
173
  // were never recorded rather than letting an unread payload read as an empty one.
173
174
  found.push(...findBodyFailures(events));
174
- found.push(...findEchoMismatches(events));
175
+ found.push(...findEchoMismatches(events, options.actionSince));
175
176
  // ── A money value written back at the wrong SCALE ───────────────────────────────────────────
176
177
  found.push(...findUnitMismatches(events, options.prior ?? []));
177
178
  // ── The action landed on something that does not react ──────────────────────────────────────
@@ -192,6 +193,9 @@ export function findContradictions(allEvents, options = {}) {
192
193
  ];
193
194
  }
194
195
  const failed = settled.filter((c) => false === c.ok);
196
+ // The failures NOBODY declared — see ContradictionOptions.expectedFailures. Only the heuristic
197
+ // "the UI moved while a request failed" rule reads this; the sharp rules still read `failed`.
198
+ const unexpected = failed.filter((c) => !matchesDeclaredFailure(c, options.expectedFailures ?? []));
195
199
  const advanced = uiAdvanced(events);
196
200
  const signals = events
197
201
  .filter((e) => e.type === EventType.SIGNAL)
@@ -215,7 +219,7 @@ export function findContradictions(allEvents, options = {}) {
215
219
  e.type === EventType.DOM_REMOVED ||
216
220
  e.type === EventType.DOM_TEXT);
217
221
  const fetched = events.some((e) => e.type === EventType.NET_REQUEST || e.type === EventType.NET_PENDING);
218
- if (routed && !rendered && !fetched) {
222
+ if (routed && !rendered && !fetched && true !== options.renderProved) {
219
223
  found.push({
220
224
  kind: ContradictionKind.ROUTE_RENDERED_NOTHING,
221
225
  claim: 'the app navigated to a new route',
@@ -259,12 +263,12 @@ export function findContradictions(allEvents, options = {}) {
259
263
  detail: failed.map(describe).join('; '),
260
264
  });
261
265
  }
262
- else if (failed.length > 0 && advanced && !misattributed && !failureAcknowledged(events)) {
266
+ else if (unexpected.length > 0 && advanced && !misattributed && !failureAcknowledged(events)) {
263
267
  found.push({
264
268
  kind: ContradictionKind.UI_ADVANCED_REQUEST_FAILED,
265
269
  claim: 'the UI moved forward (DOM/store/route changed)',
266
- counter: `${String(failed.length)} request(s) in the same window failed`,
267
- detail: failed.map(describe).join('; '),
270
+ counter: `${String(unexpected.length)} request(s) in the same window failed`,
271
+ detail: unexpected.map(describe).join('; '),
268
272
  });
269
273
  }
270
274
  // ── A write succeeded and nothing on the client moved ───────────────────────────────────────
@@ -281,23 +285,32 @@ export function findContradictions(allEvents, options = {}) {
281
285
  });
282
286
  }
283
287
  }
284
- // ── The same write fired more than once ─────────────────────────────────────────────────────
285
- const writeCounts = new Map();
286
- for (const call of settled) {
287
- if (!isMutating(call))
288
- continue;
289
- const key = `${call.method} ${call.url}`;
290
- writeCounts.set(key, (writeCounts.get(key) ?? 0) + 1);
291
- }
292
- for (const [key, count] of writeCounts) {
293
- if (count < 2)
294
- continue;
295
- found.push({
296
- kind: ContradictionKind.DUPLICATE_REQUEST,
297
- claim: 'one user action was performed',
298
- counter: `the same write fired ${String(count)} times`,
299
- detail: `${key} ×${String(count)}`,
300
- });
288
+ // ── The same write fired more than once, inside ONE action's window ─────────────────────────
289
+ // Attribution is the whole rule here, not a refinement of it: counting `method + url` over whatever
290
+ // window the caller handed in turns two legitimate separate saves into a double submit. See
291
+ // ContradictionOptions.actionSince.
292
+ const actionSince = options.actionSince;
293
+ if (actionSince !== undefined) {
294
+ const writeCounts = new Map();
295
+ for (const event of events) {
296
+ if (event.type !== EventType.NET_REQUEST || event.t < actionSince)
297
+ continue;
298
+ const call = netCall(event);
299
+ if (!isMutating(call))
300
+ continue;
301
+ const key = `${call.method} ${call.url}`;
302
+ writeCounts.set(key, (writeCounts.get(key) ?? 0) + 1);
303
+ }
304
+ for (const [key, count] of writeCounts) {
305
+ if (count < 2)
306
+ continue;
307
+ found.push({
308
+ kind: ContradictionKind.DUPLICATE_REQUEST,
309
+ claim: 'one user action was performed',
310
+ counter: `the same write fired ${String(count)} times`,
311
+ detail: `${key} ×${String(count)}`,
312
+ });
313
+ }
301
314
  }
302
315
  // ── The UI advanced over a request that never came back ─────────────────────────────────────
303
316
  // Gated on the UI having moved: an in-flight request while the app is still visibly waiting is
@@ -0,0 +1,45 @@
1
+ /**
2
+ * What the caller DECLARED before acting — the oracle, read back.
3
+ *
4
+ * `act_and_wait` asks the agent to name the expected consequence BEFORE the action, and that
5
+ * declaration was then thrown away: everything downstream judged the window as if nobody had said
6
+ * what they were expecting. Two measured consequences, one bug:
7
+ *
8
+ * - An error path can be declared exactly — `{ net, POST, /api/login, status: 500 }` — and the
9
+ * contradiction hunter still reported `ui-advanced-request-failed`, because a failed request plus
10
+ * a moved DOM is all it can see. The failure was EXPECTED and said so in the predicate, so a
11
+ * verdict of "channels disagree" is a statement about a disagreement nobody had.
12
+ * - A destination whose content was asserted and FOUND was still reported as
13
+ * `route-rendered-nothing`, a clause the element evidence beside it disproves.
14
+ *
15
+ * Deliberately conservative about what counts as declared. Only what the caller REQUIRED is read:
16
+ * the top level and `allOf` chains. An `anyOf` branch may never have held, and a `not` declares the
17
+ * opposite of a consequence — honouring either would suppress a real finding on the strength of
18
+ * something that never happened. Pure: a predicate in, a description out.
19
+ */
20
+ import type { Predicate } from './predicate-eval.js';
21
+ /** A failing call the caller named in advance — matched against the window's real calls. */
22
+ export interface DeclaredNetFailure {
23
+ method?: string;
24
+ urlContains?: string;
25
+ status?: number;
26
+ }
27
+ export interface DeclaredExpectations {
28
+ /** Requests the caller declared would FAIL, so failing is the expected outcome, not a disagreement. */
29
+ netFailures: readonly DeclaredNetFailure[];
30
+ /** The caller required something to be ON SCREEN — an element or text, present rather than absent. */
31
+ rendersContent: boolean;
32
+ }
33
+ export declare function declaredExpectations(predicate: Predicate | undefined): DeclaredExpectations;
34
+ /**
35
+ * Does a call the window recorded match a failure the caller declared?
36
+ *
37
+ * Every field the caller named must agree — a declaration about `POST /api/login → 500` says nothing
38
+ * about `POST /api/orders`, and treating it as a blanket amnesty for failed requests would remove
39
+ * the check rather than inform it. Fields the caller left out are not constraints.
40
+ */
41
+ export declare function matchesDeclaredFailure(call: {
42
+ method: string;
43
+ url: string;
44
+ status: number | undefined;
45
+ }, declared: readonly DeclaredNetFailure[]): boolean;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * What the caller DECLARED before acting — the oracle, read back.
3
+ *
4
+ * `act_and_wait` asks the agent to name the expected consequence BEFORE the action, and that
5
+ * declaration was then thrown away: everything downstream judged the window as if nobody had said
6
+ * what they were expecting. Two measured consequences, one bug:
7
+ *
8
+ * - An error path can be declared exactly — `{ net, POST, /api/login, status: 500 }` — and the
9
+ * contradiction hunter still reported `ui-advanced-request-failed`, because a failed request plus
10
+ * a moved DOM is all it can see. The failure was EXPECTED and said so in the predicate, so a
11
+ * verdict of "channels disagree" is a statement about a disagreement nobody had.
12
+ * - A destination whose content was asserted and FOUND was still reported as
13
+ * `route-rendered-nothing`, a clause the element evidence beside it disproves.
14
+ *
15
+ * Deliberately conservative about what counts as declared. Only what the caller REQUIRED is read:
16
+ * the top level and `allOf` chains. An `anyOf` branch may never have held, and a `not` declares the
17
+ * opposite of a consequence — honouring either would suppress a real finding on the strength of
18
+ * something that never happened. Pure: a predicate in, a description out.
19
+ */
20
+ import { PredicateKind } from '@reticlehq/core';
21
+ /** Below this, a status is a success or a redirect: not a declared failure. */
22
+ const FAILURE_STATUS_MIN = 400;
23
+ export function declaredExpectations(predicate) {
24
+ const netFailures = [];
25
+ let rendersContent = false;
26
+ const walk = (p) => {
27
+ switch (p.kind) {
28
+ case PredicateKind.ALL_OF:
29
+ for (const child of p.predicates)
30
+ walk(child);
31
+ return;
32
+ case PredicateKind.NET: {
33
+ const declaredFailure = false === p.ok || (p.status !== undefined && p.status >= FAILURE_STATUS_MIN);
34
+ if (!declaredFailure)
35
+ return;
36
+ netFailures.push({
37
+ ...(p.method === undefined ? {} : { method: p.method }),
38
+ ...(p.urlContains === undefined ? {} : { urlContains: p.urlContains }),
39
+ ...(p.status === undefined ? {} : { status: p.status }),
40
+ });
41
+ return;
42
+ }
43
+ case PredicateKind.ELEMENT:
44
+ case PredicateKind.TEXT:
45
+ if (true !== p.absent)
46
+ rendersContent = true;
47
+ return;
48
+ default:
49
+ return;
50
+ }
51
+ };
52
+ if (predicate !== undefined)
53
+ walk(predicate);
54
+ return { netFailures, rendersContent };
55
+ }
56
+ /**
57
+ * Does a call the window recorded match a failure the caller declared?
58
+ *
59
+ * Every field the caller named must agree — a declaration about `POST /api/login → 500` says nothing
60
+ * about `POST /api/orders`, and treating it as a blanket amnesty for failed requests would remove
61
+ * the check rather than inform it. Fields the caller left out are not constraints.
62
+ */
63
+ export function matchesDeclaredFailure(call, declared) {
64
+ return declared.some((d) => {
65
+ if (d.method !== undefined && d.method.toUpperCase() !== call.method.toUpperCase())
66
+ return false;
67
+ if (d.urlContains !== undefined && !call.url.includes(d.urlContains))
68
+ return false;
69
+ if (d.status !== undefined && d.status !== call.status)
70
+ return false;
71
+ return true;
72
+ });
73
+ }
@@ -1,4 +1,10 @@
1
1
  import { type ReticleEvent } from '@reticlehq/core';
2
2
  import type { Contradiction } from './contradictions.js';
3
- /** Contradictions for writes whose response echoes a different value than the request asked for. */
4
- export declare function findEchoMismatches(events: readonly ReticleEvent[]): Contradiction[];
3
+ /**
4
+ * Contradictions for writes whose response echoes a different value than the request asked for.
5
+ *
6
+ * `actionSince` is the attribution floor `findContradictions` already keeps — the event time the
7
+ * action that opened this window was dispatched. A finding about a request that had already settled
8
+ * before the agent acted otherwise reads as a verdict on the change it just made.
9
+ */
10
+ export declare function findEchoMismatches(events: readonly ReticleEvent[], actionSince?: number): Contradiction[];
@@ -1,4 +1,4 @@
1
- import { ContradictionKind, EventType } from '@reticlehq/core';
1
+ import { ContradictionKind, EventType, MUTATING_METHODS } from '@reticlehq/core';
2
2
  /**
3
3
  * A write that returned OK and quietly did not apply part of what it was asked to.
4
4
  *
@@ -18,6 +18,11 @@ import { ContradictionKind, EventType } from '@reticlehq/core';
18
18
  * would fire on healthy APIs constantly, and a contradiction kind that cries wolf gets filtered out
19
19
  * by the agents reading it, taking the true positives with it. So:
20
20
  *
21
+ * - only requests that are WRITES are considered. Reported from the field: a lookup that sends its
22
+ * key in the body (`POST /get-branding` with `{workspace_id}`) and gets a record back was graded
23
+ * as a half-applied write. The gate was on the RESPONSE being a success and never on the REQUEST
24
+ * being a write, so every successful call carrying a body read as a save — and the same key name
25
+ * legitimately means different things on the two sides of a request/response pair;
21
26
  * - only keys the request actually SENT are considered;
22
27
  * - only scalars, since deep structural diffing is where the false positives live;
23
28
  * - values are compared NORMALISED (trimmed, case-folded, numbers as numbers), so `FR` vs `fr` and
@@ -89,12 +94,33 @@ function scalarsByKey(body, into = new Map(), depth = 0) {
89
94
  }
90
95
  const OK_MIN = 200;
91
96
  const OK_MAX = 300;
92
- /** Contradictions for writes whose response echoes a different value than the request asked for. */
93
- export function findEchoMismatches(events) {
97
+ /**
98
+ * Contradictions for writes whose response echoes a different value than the request asked for.
99
+ *
100
+ * `actionSince` is the attribution floor `findContradictions` already keeps — the event time the
101
+ * action that opened this window was dispatched. A finding about a request that had already settled
102
+ * before the agent acted otherwise reads as a verdict on the change it just made.
103
+ */
104
+ export function findEchoMismatches(events, actionSince) {
94
105
  const found = [];
95
106
  for (const event of events) {
96
107
  if (event.type !== EventType.NET_REQUEST)
97
108
  continue;
109
+ // `MUTATING_METHODS` is the one definition of "is this a write" in the codebase — the same list
110
+ // `contradictions.ts`'s `isMutating` reads. A read has no fields to half-apply.
111
+ //
112
+ // KNOWN LIMIT, stated because it is easy to read this fix as bigger than it is: POST is in that
113
+ // list, so a lookup that POSTs its key and gets a record back — the shape actually reported —
114
+ // still reaches the comparison below. Method is the only signal available here, and a read-shaped
115
+ // POST is genuinely indistinguishable from a write by method alone.
116
+ //
117
+ // The tempting next step is to skip a pair whose JSON types disagree (a string key sent, a number
118
+ // echoed). Deliberately not taken: that also silences `{"qty":"5"}` answered with `3`, which is a
119
+ // real dropped write. A false negative here is a missed bug in someone's app, and that is not a
120
+ // trade this file makes to quieten a false positive.
121
+ const method = 'string' === typeof event.data['method'] ? event.data['method'] : '';
122
+ if (!MUTATING_METHODS.includes(method.toUpperCase()))
123
+ continue;
98
124
  // IPC reports `ok` rather than a status; HTTP reports a status. Either must say success — a write
99
125
  // that already failed is a different (and louder) finding than one that half-applied.
100
126
  const status = event.data['status'];
@@ -126,12 +152,14 @@ export function findEchoMismatches(events) {
126
152
  if (0 === dropped.length)
127
153
  continue;
128
154
  const url = 'string' === typeof event.data['url'] ? event.data['url'] : '';
129
- const method = 'string' === typeof event.data['method'] ? event.data['method'] : '';
155
+ const attribution = actionSince !== undefined && event.t < actionSince
156
+ ? ' — this request completed before the action being verified, so the finding is not about that change'
157
+ : '';
130
158
  found.push({
131
159
  kind: ContradictionKind.WRITE_FIELD_IGNORED,
132
160
  claim: 'the write returned success and the page treated it as saved',
133
161
  counter: `its own echo shows ${String(dropped.length)} field(s) NOT applied — ${dropped.join('; ')}`,
134
- detail: `${method} ${url} — the server answered OK and returned a different value than it was asked to set, so the write half-applied; the UI has no way to know and will show the value the user typed`,
162
+ detail: `${method} ${url} — the server answered OK and returned a different value than it was asked to set, so the write half-applied; the UI has no way to know and will show the value the user typed${attribution}`,
135
163
  });
136
164
  }
137
165
  return found;
@@ -57,7 +57,7 @@ interface NetCallView {
57
57
  */
58
58
  oneWay?: boolean;
59
59
  }
60
- export declare function projectNetCall(e: ReticleEvent): NetCallView;
60
+ export declare function projectNetCall(e: ReticleEvent, includeBodies?: boolean): NetCallView;
61
61
  /**
62
62
  * Compact console-log summary for reticle_console output.
63
63
  *
@@ -86,7 +86,7 @@ export function reconcileNet(events) {
86
86
  .map((e) => ({ ...e, data: { ...e.data, status: 'pending', pending: true } }));
87
87
  return [...completed, ...unresolved].sort((a, b) => a.t - b.t);
88
88
  }
89
- export function projectNetCall(e) {
89
+ export function projectNetCall(e, includeBodies = true) {
90
90
  const status = e.data['status'];
91
91
  const ms = asNumber(e.data['durationMs']);
92
92
  const statusText = asString(e.data['statusText']);
@@ -106,11 +106,14 @@ export function projectNetCall(e) {
106
106
  view.contentType = contentType;
107
107
  if (responseSize !== undefined)
108
108
  view.responseSize = responseSize;
109
- if (requestBody !== undefined)
109
+ // Bodies dominate the payload of the most-called read tool, and are only occasionally the part
110
+ // anyone wanted — `bodies: false` on reticle_network drops them for a body-free listing (#401).
111
+ if (includeBodies && requestBody !== undefined)
110
112
  view.requestBody = requestBody;
111
- if (responseBody !== undefined)
113
+ if (includeBodies && responseBody !== undefined)
112
114
  view.responseBody = responseBody;
113
- if (true === e.data['requestBodyTruncated'] || true === e.data['responseBodyTruncated']) {
115
+ if (includeBodies &&
116
+ (true === e.data['requestBodyTruncated'] || true === e.data['responseBodyTruncated'])) {
114
117
  view.bodyTruncated = true;
115
118
  }
116
119
  if (ms !== undefined)
@@ -1,72 +1,6 @@
1
- import { ElementState, PredicateKind, type ElementQuery, type ReticleEvent } from '@reticlehq/core';
2
- import { z } from 'zod';
3
- export type Predicate = {
4
- kind: typeof PredicateKind.ELEMENT;
5
- query: ElementQuery;
6
- state?: ElementState;
7
- absent?: boolean;
8
- } | {
9
- kind: typeof PredicateKind.TEXT;
10
- contains: string;
11
- visible?: boolean;
12
- absent?: boolean;
13
- } | {
14
- kind: typeof PredicateKind.NET;
15
- method?: string;
16
- urlContains?: string;
17
- status?: number;
18
- /** Did the call succeed? The honest field for IPC, which has no status code. */
19
- ok?: boolean;
20
- since?: number;
21
- count?: number;
22
- } | {
23
- kind: typeof PredicateKind.ROUTE;
24
- pathname?: string;
25
- contains?: string;
26
- since?: number;
27
- } | {
28
- kind: typeof PredicateKind.CONSOLE;
29
- level?: string;
30
- absent?: boolean;
31
- since?: number;
32
- } | {
33
- kind: typeof PredicateKind.ANIMATION;
34
- name?: string;
35
- target?: string;
36
- completed?: boolean;
37
- since?: number;
38
- } | {
39
- kind: typeof PredicateKind.SIGNAL;
40
- name?: string;
41
- dataMatches?: Record<string, unknown>;
42
- since?: number;
43
- } | {
44
- kind: typeof PredicateKind.STATE;
45
- store?: string;
46
- path: string;
47
- equals?: unknown;
48
- } | {
49
- kind: typeof PredicateKind.SETTLED;
50
- quietMs?: number;
51
- } | {
52
- kind: typeof PredicateKind.ALL_OF;
53
- predicates: Predicate[];
54
- } | {
55
- kind: typeof PredicateKind.ANY_OF;
56
- predicates: Predicate[];
57
- } | {
58
- kind: typeof PredicateKind.NOT;
59
- predicate: Predicate;
60
- };
61
- export declare const PredicateSchema: z.ZodType<Predicate>;
62
- /**
63
- * The fields a given predicate kind accepts, read off the schema itself.
64
- *
65
- * Derived rather than listed so the two can never disagree: a rejection message that names a stale
66
- * field set is worse than one that names none, because the agent trusts it and retries into the same
67
- * wall. Empty for a kind that is not in the union.
68
- */
69
- export declare function predicateFieldsFor(kind: string): readonly string[];
1
+ import { PredicateKind, type ReticleEvent } from '@reticlehq/core';
2
+ import type { Predicate } from './predicate-schema.js';
3
+ export * from './predicate-schema.js';
70
4
  export interface EvalResult {
71
5
  pass: boolean;
72
6
  /**
@@ -85,6 +19,19 @@ export interface EvalResult {
85
19
  * not a defect in the user's app, and reporting it as one puts agent mistakes into the bug count.
86
20
  */
87
21
  inconclusive?: string;
22
+ /**
23
+ * No later event in this window can change this answer, so waiting out the rest of the budget
24
+ * buys nothing.
25
+ *
26
+ * Set ONLY where that is provable, which is rarer than it looks. "The toast never appeared" is not
27
+ * decided — it is only knowable when the budget ends, and ending early there would manufacture the
28
+ * false negative the budget exists to prevent. Exact cardinality IS decided once exceeded, because
29
+ * a window only accumulates matches and a count cannot come back down.
30
+ *
31
+ * A scheduling fact, never a verdict: `pass` already says what the answer is, and this only says
32
+ * that it is final.
33
+ */
34
+ decided?: boolean;
88
35
  evidence?: unknown;
89
36
  failureReason?: string;
90
37
  /**
@@ -113,6 +60,7 @@ export interface EvalResult {
113
60
  expected?: string;
114
61
  assertion?: string;
115
62
  }
63
+ export declare function str(value: unknown): string | undefined;
116
64
  /**
117
65
  * Match one value against a pattern. Supports `*` (present), strict equality, and operators:
118
66
  * `{$gte,$lte,$gt,$lt}` (numbers), `{$contains}` (array membership or substring), `{$length}`.
@@ -121,9 +69,6 @@ export declare function matchValue(got: unknown, want: unknown): boolean;
121
69
  export declare function evalNet(events: ReticleEvent[], p: Extract<Predicate, {
122
70
  kind: typeof PredicateKind.NET;
123
71
  }>): EvalResult;
124
- export declare function evalRoute(events: ReticleEvent[], p: Extract<Predicate, {
125
- kind: typeof PredicateKind.ROUTE;
126
- }>): EvalResult;
127
72
  export declare function evalConsole(events: ReticleEvent[], p: Extract<Predicate, {
128
73
  kind: typeof PredicateKind.CONSOLE;
129
74
  }>): EvalResult;