@reticlehq/server 2.11.0 → 2.13.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 (513) hide show
  1. package/SKILL.md +93 -111
  2. package/dist/bridge/auth-failure-reason.d.ts +6 -2
  3. package/dist/bridge/auth-failure-reason.js +15 -8
  4. package/dist/bridge/bridge.d.ts +18 -14
  5. package/dist/bridge/bridge.js +248 -165
  6. package/dist/bridge/bridge.test-harness.d.ts +2 -0
  7. package/dist/bridge/bridge.test-harness.js +8 -1
  8. package/dist/bridge/pairing-token.d.ts +5 -0
  9. package/dist/bridge/pairing-token.js +28 -0
  10. package/dist/bridge/protocol-skew.d.ts +37 -0
  11. package/dist/bridge/protocol-skew.js +45 -0
  12. package/dist/capsule/capsule-store.d.ts +8 -7
  13. package/dist/capsule/capsule-store.js +1 -1
  14. package/dist/capsule/causal-summary.d.ts +2 -2
  15. package/dist/capsule/causal-summary.js +7 -2
  16. package/dist/chromium-launch-options.d.ts +28 -0
  17. package/dist/chromium-launch-options.js +29 -0
  18. package/dist/cli/cli-doctor.js +39 -1
  19. package/dist/cli/cli-flow-commands.d.ts +3 -1
  20. package/dist/cli/cli-flow-commands.js +34 -4
  21. package/dist/cli/cli-kill.d.ts +2 -1
  22. package/dist/cli/cli-launch.d.ts +11 -1
  23. package/dist/cli/cli-launch.js +59 -10
  24. package/dist/cli/cli-parse.d.ts +17 -1
  25. package/dist/cli/cli-parse.js +238 -12
  26. package/dist/cli/cli-update-commands.js +20 -1
  27. package/dist/cli/cli-verify.d.ts +2 -1
  28. package/dist/cli/cli-verify.js +5 -4
  29. package/dist/cli/cloud-cli.js +428 -253
  30. package/dist/cli/cloud-keystore.d.ts +37 -0
  31. package/dist/cli/cloud-keystore.js +97 -0
  32. package/dist/cli/cloud-kit.d.ts +37 -0
  33. package/dist/cli/cloud-kit.js +121 -0
  34. package/dist/cli/cloud-login.d.ts +17 -0
  35. package/dist/cli/cloud-login.js +183 -0
  36. package/dist/cli/cloud-session.d.ts +48 -0
  37. package/dist/cli/cloud-session.js +53 -0
  38. package/dist/cli/config-discovery.d.ts +2 -1
  39. package/dist/cli/config-discovery.js +1 -0
  40. package/dist/cli/daemon-start-options.d.ts +32 -0
  41. package/dist/cli/daemon-start-options.js +38 -0
  42. package/dist/cli/device-flow.d.ts +76 -0
  43. package/dist/cli/device-flow.js +56 -0
  44. package/dist/cli/doctor-rows.d.ts +12 -1
  45. package/dist/cli/doctor-rows.js +12 -1
  46. package/dist/cli/drive-attach.d.ts +13 -0
  47. package/dist/cli/drive-attach.js +18 -0
  48. package/dist/cli/drive-command.js +7 -3
  49. package/dist/cli/gate-exit.d.ts +21 -0
  50. package/dist/cli/gate-exit.js +21 -0
  51. package/dist/cli/gate-hook-message.d.ts +33 -0
  52. package/dist/cli/gate-hook-message.js +46 -0
  53. package/dist/cli/mcp-command.d.ts +25 -0
  54. package/dist/cli/mcp-command.js +174 -0
  55. package/dist/cli/project-name.d.ts +30 -0
  56. package/dist/cli/project-name.js +44 -0
  57. package/dist/cli/sibling-ports.d.ts +37 -0
  58. package/dist/cli/sibling-ports.js +56 -0
  59. package/dist/cli/status-next-action.d.ts +14 -0
  60. package/dist/cli/status-next-action.js +4 -4
  61. package/dist/cli.d.ts +1 -0
  62. package/dist/cli.js +167 -186
  63. package/dist/cloud/cloud-config.d.ts +24 -2
  64. package/dist/cloud/cloud-config.js +64 -5
  65. package/dist/cloud/cloud-sync.d.ts +16 -5
  66. package/dist/cloud/cloud-sync.js +64 -1
  67. package/dist/cloud/sync-cycle.d.ts +110 -0
  68. package/dist/cloud/sync-cycle.js +270 -0
  69. package/dist/cloud/sync-daemon.d.ts +69 -0
  70. package/dist/cloud/sync-daemon.js +260 -0
  71. package/dist/cloud/sync-disk.d.ts +9 -0
  72. package/dist/cloud/sync-disk.js +191 -0
  73. package/dist/cloud/sync-hash.d.ts +2 -0
  74. package/dist/cloud/sync-hash.js +35 -0
  75. package/dist/crawl/crawl-tools.js +5 -3
  76. package/dist/crawl/crawl.d.ts +12 -13
  77. package/dist/crawl/crawl.js +25 -3
  78. package/dist/daemon/daemon-resolve.d.ts +122 -0
  79. package/dist/daemon/daemon-resolve.js +187 -0
  80. package/dist/daemon/dev-servers.d.ts +15 -0
  81. package/dist/daemon/dev-servers.js +68 -0
  82. package/dist/daemon/free-port.d.ts +29 -0
  83. package/dist/daemon/free-port.js +55 -0
  84. package/dist/daemon/heartbeat.d.ts +0 -4
  85. package/dist/daemon/heartbeat.js +2 -2
  86. package/dist/daemon/wake-decision.d.ts +32 -0
  87. package/dist/daemon/wake-decision.js +38 -0
  88. package/dist/dev/stale-issue-guard.d.ts +0 -2
  89. package/dist/dev/stale-issue-guard.js +1 -1
  90. package/dist/domain/domain-tools.js +2 -2
  91. package/dist/events/contradictions.d.ts +59 -0
  92. package/dist/events/contradictions.js +253 -27
  93. package/dist/events/declared.d.ts +17 -0
  94. package/dist/events/declared.js +43 -1
  95. package/dist/events/echo-mismatch.js +65 -1
  96. package/dist/events/event-filters.d.ts +8 -0
  97. package/dist/events/event-filters.js +19 -2
  98. package/dist/events/observed-in-window.d.ts +12 -0
  99. package/dist/events/observed-in-window.js +17 -0
  100. package/dist/events/predicate-asks.d.ts +13 -0
  101. package/dist/events/predicate-asks.js +22 -0
  102. package/dist/events/predicate-eval.js +117 -27
  103. package/dist/events/predicate-parse.d.ts +0 -6
  104. package/dist/events/predicate-parse.js +43 -3
  105. package/dist/events/predicate-precheck.d.ts +8 -0
  106. package/dist/events/predicate-precheck.js +41 -0
  107. package/dist/events/predicate-route.d.ts +14 -0
  108. package/dist/events/predicate-route.js +36 -8
  109. package/dist/events/predicate-schema.d.ts +44 -0
  110. package/dist/events/predicate-schema.js +126 -23
  111. package/dist/events/predicate.d.ts +20 -0
  112. package/dist/events/predicate.js +77 -6
  113. package/dist/events/split-text-miss.d.ts +30 -0
  114. package/dist/events/split-text-miss.js +44 -0
  115. package/dist/flows/coverage.d.ts +0 -8
  116. package/dist/flows/coverage.js +1 -1
  117. package/dist/flows/decision.d.ts +5 -1
  118. package/dist/flows/decision.js +33 -7
  119. package/dist/flows/flake.d.ts +6 -8
  120. package/dist/flows/flake.js +1 -1
  121. package/dist/flows/flow-intent.d.ts +52 -0
  122. package/dist/flows/flow-intent.js +123 -0
  123. package/dist/flows/flow-memory-consult.d.ts +55 -0
  124. package/dist/flows/flow-memory-consult.js +38 -0
  125. package/dist/flows/flow-replay-run.d.ts +18 -7
  126. package/dist/flows/flow-replay-run.js +263 -23
  127. package/dist/flows/flow-replay.js +11 -2
  128. package/dist/flows/flow-step-runners.d.ts +8 -0
  129. package/dist/flows/flow-step-runners.js +8 -2
  130. package/dist/flows/flow-store-for-session.d.ts +31 -0
  131. package/dist/flows/flow-store-for-session.js +33 -0
  132. package/dist/flows/flow-success.js +15 -3
  133. package/dist/flows/flow-tools.js +87 -33
  134. package/dist/flows/flows.d.ts +14 -1
  135. package/dist/flows/flows.js +502 -24
  136. package/dist/flows/heal-run.js +5 -2
  137. package/dist/flows/predicate-to-expect.d.ts +17 -12
  138. package/dist/flows/predicate-to-expect.js +36 -15
  139. package/dist/flows/recordings.d.ts +13 -1
  140. package/dist/flows/recordings.js +17 -2
  141. package/dist/flows/replay.d.ts +22 -2
  142. package/dist/flows/replay.js +28 -2
  143. package/dist/flows/server-verify.js +16 -2
  144. package/dist/flows/verify-change-tools.js +9 -3
  145. package/dist/honesty/blind-spots.d.ts +51 -15
  146. package/dist/honesty/blind-spots.js +64 -4
  147. package/dist/honesty/feature-capture.d.ts +150 -0
  148. package/dist/honesty/feature-capture.js +233 -0
  149. package/dist/honesty/honesty.d.ts +24 -0
  150. package/dist/honesty/honesty.js +25 -1
  151. package/dist/honesty/instrumentation-gaps.d.ts +39 -6
  152. package/dist/honesty/instrumentation-gaps.js +88 -5
  153. package/dist/honesty/tool-hit-rate.d.ts +86 -0
  154. package/dist/honesty/tool-hit-rate.js +90 -0
  155. package/dist/honesty/undeclared-change.d.ts +51 -0
  156. package/dist/honesty/undeclared-change.js +56 -0
  157. package/dist/honesty/verified.d.ts +16 -0
  158. package/dist/honesty/verified.js +84 -5
  159. package/dist/http-server.d.ts +0 -1
  160. package/dist/http-server.js +13 -1
  161. package/dist/hunt/hunt-report.d.ts +0 -2
  162. package/dist/hunt/hunt-report.js +1 -1
  163. package/dist/impact/impact-recorder.d.ts +31 -8
  164. package/dist/impact/impact-recorder.js +104 -21
  165. package/dist/impact/impact-store.d.ts +13 -4
  166. package/dist/impact/impact-store.js +44 -3
  167. package/dist/index.d.ts +4 -1
  168. package/dist/index.js +113 -7
  169. package/dist/init/agent-rules.d.ts +30 -6
  170. package/dist/init/agent-rules.js +54 -13
  171. package/dist/init/astro-patch.d.ts +16 -0
  172. package/dist/init/astro-patch.js +28 -0
  173. package/dist/init/capabilities.d.ts +4 -3
  174. package/dist/init/capabilities.js +23 -3
  175. package/dist/init/closing-hint.d.ts +36 -0
  176. package/dist/init/closing-hint.js +120 -0
  177. package/dist/init/confirm.d.ts +27 -28
  178. package/dist/init/confirm.js +51 -11
  179. package/dist/init/cra.d.ts +34 -5
  180. package/dist/init/cra.js +96 -25
  181. package/dist/init/csp-check.js +17 -2
  182. package/dist/init/csp-doctor.d.ts +8 -1
  183. package/dist/init/csp-doctor.js +8 -3
  184. package/dist/init/dev-script.d.ts +50 -0
  185. package/dist/init/dev-script.js +66 -0
  186. package/dist/init/format-generated.d.ts +18 -0
  187. package/dist/init/format-generated.js +39 -0
  188. package/dist/init/install-hint.d.ts +9 -0
  189. package/dist/init/install-hint.js +29 -0
  190. package/dist/init/mcp-clients.d.ts +7 -0
  191. package/dist/init/mcp-clients.js +7 -0
  192. package/dist/init/mcp.d.ts +17 -6
  193. package/dist/init/mcp.js +27 -11
  194. package/dist/init/node-io.js +11 -1
  195. package/dist/init/plan-framework.d.ts +0 -5
  196. package/dist/init/plan-framework.js +147 -37
  197. package/dist/init/plan.d.ts +30 -37
  198. package/dist/init/plan.js +50 -60
  199. package/dist/init/preflight.d.ts +28 -0
  200. package/dist/init/preflight.js +38 -0
  201. package/dist/init/refresh-rules.d.ts +39 -0
  202. package/dist/init/refresh-rules.js +51 -0
  203. package/dist/init/run.d.ts +32 -35
  204. package/dist/init/run.js +151 -202
  205. package/dist/init/slash-command.d.ts +1 -1
  206. package/dist/init/snippets.js +90 -31
  207. package/dist/init/vite-config.d.ts +1 -1
  208. package/dist/init/vite-config.js +39 -25
  209. package/dist/init/workspace-apps.d.ts +17 -10
  210. package/dist/init/workspace-apps.js +110 -0
  211. package/dist/input/network-mock-tools.js +3 -6
  212. package/dist/input/real-input.d.ts +2 -0
  213. package/dist/input/real-input.js +28 -7
  214. package/dist/input/scroll-find.d.ts +7 -4
  215. package/dist/input/scroll-find.js +63 -25
  216. package/dist/input/scroll-tools.js +4 -6
  217. package/dist/input/viewport-tools.js +5 -4
  218. package/dist/intent/inline-intent.d.ts +37 -0
  219. package/dist/intent/inline-intent.js +193 -0
  220. package/dist/intent/intent-shard-store.d.ts +72 -0
  221. package/dist/intent/intent-shard-store.js +203 -0
  222. package/dist/intent/intent-shard.d.ts +411 -0
  223. package/dist/intent/intent-shard.js +176 -0
  224. package/dist/intent/intent-store.d.ts +49 -0
  225. package/dist/intent/intent-store.js +133 -0
  226. package/dist/intent/intent-subject.d.ts +45 -0
  227. package/dist/intent/intent-subject.js +177 -0
  228. package/dist/intent/intent-tools.d.ts +2 -0
  229. package/dist/intent/intent-tools.js +143 -0
  230. package/dist/intent/open-intents.d.ts +42 -0
  231. package/dist/intent/open-intents.js +40 -0
  232. package/dist/journal/envelope.d.ts +26 -26
  233. package/dist/journal/journal-recorder.d.ts +23 -0
  234. package/dist/journal/journal-recorder.js +35 -0
  235. package/dist/license/license-env.d.ts +29 -0
  236. package/dist/license/license-env.js +148 -0
  237. package/dist/loopback-agent.d.ts +31 -0
  238. package/dist/loopback-agent.js +57 -0
  239. package/dist/mcp/attach-memory.d.ts +83 -0
  240. package/dist/mcp/attach-memory.js +185 -0
  241. package/dist/mcp/client-identity.d.ts +31 -0
  242. package/dist/mcp/client-identity.js +51 -0
  243. package/dist/mcp/mcp-post-transport.d.ts +26 -0
  244. package/dist/mcp/mcp-post-transport.js +109 -0
  245. package/dist/mcp/mcp-proxy.d.ts +2 -8
  246. package/dist/mcp/mcp-proxy.js +64 -62
  247. package/dist/mcp/mcp.d.ts +2 -0
  248. package/dist/mcp/mcp.js +52 -24
  249. package/dist/mcp/proxy-backoff.d.ts +11 -0
  250. package/dist/mcp/proxy-backoff.js +13 -0
  251. package/dist/mcp/proxy-daemon-probe.d.ts +1 -0
  252. package/dist/mcp/proxy-daemon-probe.js +7 -1
  253. package/dist/mcp/proxy-handshake.d.ts +23 -0
  254. package/dist/mcp/proxy-handshake.js +31 -0
  255. package/dist/mcp/server-instructions.js +45 -3
  256. package/dist/mcp/tool-catalog-cache.d.ts +16 -0
  257. package/dist/mcp/tool-catalog-cache.js +22 -0
  258. package/dist/memory/memory-tools.d.ts +2 -0
  259. package/dist/memory/memory-tools.js +82 -0
  260. package/dist/memory/project-memory.d.ts +34 -0
  261. package/dist/memory/project-memory.js +96 -0
  262. package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
  263. package/dist/oracles/flow-instrument-gaps.js +4 -5
  264. package/dist/oracles/propose-consequences.js +12 -3
  265. package/dist/oracles/self-instrument.d.ts +9 -9
  266. package/dist/oracles/self-instrument.js +19 -13
  267. package/dist/pool/browser-pool.d.ts +75 -5
  268. package/dist/pool/browser-pool.js +144 -5
  269. package/dist/pool/playwright-launcher.js +12 -1
  270. package/dist/project/fs-port.d.ts +3 -0
  271. package/dist/project/fs-port.js +3 -2
  272. package/dist/project/memory-fs.js +6 -1
  273. package/dist/project/project-tools.js +6 -35
  274. package/dist/project/reticle-dir.d.ts +17 -2
  275. package/dist/project/reticle-dir.js +22 -4
  276. package/dist/runs/context-tools.d.ts +2 -0
  277. package/dist/runs/context-tools.js +89 -0
  278. package/dist/runs/run-context.d.ts +40 -0
  279. package/dist/runs/run-context.js +158 -0
  280. package/dist/runs/run-diff.d.ts +2 -1
  281. package/dist/runs/run-store.d.ts +10 -0
  282. package/dist/runs/run-store.js +10 -0
  283. package/dist/runs/verification-sync.d.ts +0 -11
  284. package/dist/runs/verification-sync.js +24 -3
  285. package/dist/runs/verify-server.d.ts +8 -1
  286. package/dist/runs/verify-server.js +14 -2
  287. package/dist/session/command-timeout.d.ts +5 -2
  288. package/dist/session/command-timeout.js +5 -4
  289. package/dist/session/dev-server-probe.d.ts +0 -8
  290. package/dist/session/dev-server-probe.js +1 -1
  291. package/dist/session/last-act.d.ts +7 -1
  292. package/dist/session/last-act.js +2 -2
  293. package/dist/session/lease-visibility.d.ts +67 -0
  294. package/dist/session/lease-visibility.js +76 -0
  295. package/dist/session/live-control-tools.js +1 -42
  296. package/dist/session/no-session-diagnosis.d.ts +33 -0
  297. package/dist/session/no-session-diagnosis.js +82 -24
  298. package/dist/session/no-session-next-action.d.ts +12 -0
  299. package/dist/session/no-session-next-action.js +77 -6
  300. package/dist/session/no-session-watch.d.ts +18 -6
  301. package/dist/session/no-session-watch.js +125 -75
  302. package/dist/session/output-budget.js +4 -1
  303. package/dist/session/pending-commands.d.ts +11 -1
  304. package/dist/session/pending-commands.js +12 -2
  305. package/dist/session/resolve-within.d.ts +34 -0
  306. package/dist/session/resolve-within.js +53 -0
  307. package/dist/session/session-health.d.ts +9 -0
  308. package/dist/session/session-health.js +23 -0
  309. package/dist/session/session-info.d.ts +18 -0
  310. package/dist/session/session-info.js +11 -0
  311. package/dist/session/session-manager.d.ts +14 -0
  312. package/dist/session/session-manager.js +111 -4
  313. package/dist/session/session-mirror.d.ts +16 -0
  314. package/dist/session/session-mirror.js +22 -0
  315. package/dist/session/session-recommendation.d.ts +19 -5
  316. package/dist/session/session-recommendation.js +25 -9
  317. package/dist/session/session-successor.d.ts +37 -0
  318. package/dist/session/session-successor.js +113 -0
  319. package/dist/session/session-tools.js +2 -2
  320. package/dist/session/session.d.ts +53 -3
  321. package/dist/session/session.js +162 -9
  322. package/dist/session/wall-clock.d.ts +12 -0
  323. package/dist/session/wall-clock.js +14 -0
  324. package/dist/setup/agent-configs.d.ts +96 -0
  325. package/dist/setup/agent-configs.js +333 -0
  326. package/dist/setup/agent-io.d.ts +9 -0
  327. package/dist/setup/agent-io.js +16 -0
  328. package/dist/setup/agent-writer.d.ts +51 -0
  329. package/dist/setup/agent-writer.js +115 -0
  330. package/dist/setup/approval-migration.d.ts +47 -0
  331. package/dist/setup/approval-migration.js +68 -0
  332. package/dist/setup/auto-approve.d.ts +85 -0
  333. package/dist/setup/auto-approve.js +150 -0
  334. package/dist/setup/bridge-port.d.ts +15 -0
  335. package/dist/setup/bridge-port.js +24 -0
  336. package/dist/setup/desktop-shape.d.ts +57 -0
  337. package/dist/setup/desktop-shape.js +63 -0
  338. package/dist/setup/dev-server-wait.d.ts +60 -0
  339. package/dist/setup/dev-server-wait.js +65 -0
  340. package/dist/setup/drive-agent.d.ts +71 -0
  341. package/dist/setup/drive-agent.js +171 -0
  342. package/dist/setup/drive-plan.d.ts +68 -0
  343. package/dist/setup/drive-plan.js +130 -0
  344. package/dist/setup/ensure-daemon.d.ts +29 -0
  345. package/dist/setup/ensure-daemon.js +58 -0
  346. package/dist/setup/init-runtime.d.ts +47 -0
  347. package/dist/setup/init-runtime.js +160 -0
  348. package/dist/setup/interrupt.d.ts +25 -0
  349. package/dist/setup/interrupt.js +40 -0
  350. package/dist/setup/license-key.d.ts +39 -0
  351. package/dist/setup/license-key.js +72 -0
  352. package/dist/setup/listeners.d.ts +37 -0
  353. package/dist/setup/listeners.js +85 -0
  354. package/dist/setup/node-effects.d.ts +60 -0
  355. package/dist/setup/node-effects.js +210 -0
  356. package/dist/setup/page-probe.d.ts +39 -0
  357. package/dist/setup/page-probe.js +49 -0
  358. package/dist/setup/relaunch.d.ts +36 -0
  359. package/dist/setup/relaunch.js +57 -0
  360. package/dist/setup/remaining-steps.d.ts +19 -0
  361. package/dist/setup/remaining-steps.js +31 -0
  362. package/dist/setup/run-setup.d.ts +99 -0
  363. package/dist/setup/run-setup.js +187 -0
  364. package/dist/setup/session-pick.d.ts +47 -0
  365. package/dist/setup/session-pick.js +51 -0
  366. package/dist/setup/setup-command.d.ts +45 -0
  367. package/dist/setup/setup-command.js +199 -0
  368. package/dist/setup/setup-options.d.ts +74 -0
  369. package/dist/setup/setup-options.js +67 -0
  370. package/dist/setup/transcripts.d.ts +16 -0
  371. package/dist/setup/transcripts.js +43 -0
  372. package/dist/status-payload.d.ts +14 -1
  373. package/dist/status-payload.js +23 -1
  374. package/dist/telemetry/automation-hint.d.ts +2 -1
  375. package/dist/telemetry/daemon-telemetry.d.ts +8 -1
  376. package/dist/telemetry/daemon-telemetry.js +52 -5
  377. package/dist/telemetry/error-class.js +1 -1
  378. package/dist/telemetry/feedback-context.d.ts +9 -1
  379. package/dist/telemetry/feedback-context.js +60 -29
  380. package/dist/telemetry/feedback-local.d.ts +15 -0
  381. package/dist/telemetry/feedback-local.js +56 -0
  382. package/dist/telemetry/feedback-outbox.d.ts +2 -1
  383. package/dist/telemetry/feedback.d.ts +11 -0
  384. package/dist/telemetry/feedback.js +19 -6
  385. package/dist/telemetry/init-telemetry.d.ts +2 -0
  386. package/dist/telemetry/init-telemetry.js +2 -0
  387. package/dist/telemetry/install-source.d.ts +14 -0
  388. package/dist/telemetry/install-source.js +30 -0
  389. package/dist/telemetry/license-activation.d.ts +7 -1
  390. package/dist/telemetry/license-activation.js +9 -5
  391. package/dist/telemetry/mcp-connection.js +7 -0
  392. package/dist/telemetry/project-profile.d.ts +12 -9
  393. package/dist/telemetry/project-profile.js +12 -7
  394. package/dist/telemetry/proxy-telemetry.d.ts +3 -0
  395. package/dist/telemetry/proxy-telemetry.js +21 -0
  396. package/dist/telemetry/run-telemetry.d.ts +0 -2
  397. package/dist/telemetry/run-telemetry.js +1 -1
  398. package/dist/telemetry/session-metrics.d.ts +12 -0
  399. package/dist/telemetry/session-metrics.js +35 -1
  400. package/dist/telemetry/telemetry.js +17 -2
  401. package/dist/telemetry/tool-refused.d.ts +3 -1
  402. package/dist/telemetry/tool-refused.js +34 -2
  403. package/dist/telemetry/verification-of.js +2 -2
  404. package/dist/tools/act-capsule.d.ts +10 -0
  405. package/dist/tools/act-capsule.js +8 -2
  406. package/dist/tools/act-danger.d.ts +2 -1
  407. package/dist/tools/act-danger.js +11 -3
  408. package/dist/tools/act-observation.d.ts +41 -0
  409. package/dist/tools/act-observation.js +36 -0
  410. package/dist/tools/act-preflight.d.ts +20 -0
  411. package/dist/tools/act-preflight.js +98 -0
  412. package/dist/tools/act-sequence-retry.d.ts +45 -0
  413. package/dist/tools/act-sequence-retry.js +58 -0
  414. package/dist/tools/act-sequence-tool.d.ts +13 -0
  415. package/dist/tools/act-sequence-tool.js +135 -0
  416. package/dist/tools/act-target.d.ts +14 -0
  417. package/dist/tools/act-target.js +47 -0
  418. package/dist/tools/act-tools.d.ts +21 -1
  419. package/dist/tools/act-tools.js +196 -204
  420. package/dist/tools/assert-source.d.ts +22 -0
  421. package/dist/tools/assert-source.js +53 -0
  422. package/dist/tools/assert-verdict.d.ts +20 -3
  423. package/dist/tools/assert-verdict.js +58 -20
  424. package/dist/tools/browser-tools.js +6 -22
  425. package/dist/tools/complete-snapshot.d.ts +48 -0
  426. package/dist/tools/complete-snapshot.js +75 -0
  427. package/dist/tools/coverage-tools.d.ts +10 -1
  428. package/dist/tools/coverage-tools.js +123 -69
  429. package/dist/tools/dynamic-tools.js +53 -13
  430. package/dist/tools/error-recovery.d.ts +25 -14
  431. package/dist/tools/error-recovery.js +78 -9
  432. package/dist/tools/feedback-tools.d.ts +1 -1
  433. package/dist/tools/feedback-tools.js +2 -2
  434. package/dist/tools/invoke-tool.js +121 -11
  435. package/dist/tools/lease-hint.d.ts +10 -0
  436. package/dist/tools/lease-hint.js +53 -4
  437. package/dist/tools/lease-tools.d.ts +35 -5
  438. package/dist/tools/lease-tools.js +191 -13
  439. package/dist/tools/merged-name-redirect.d.ts +13 -0
  440. package/dist/tools/merged-name-redirect.js +22 -0
  441. package/dist/tools/navigate-arrival.d.ts +0 -2
  442. package/dist/tools/navigate-arrival.js +1 -1
  443. package/dist/tools/numeric-bounds.d.ts +100 -0
  444. package/dist/tools/numeric-bounds.js +125 -0
  445. package/dist/tools/observe-tools.js +109 -65
  446. package/dist/tools/read-tools.js +31 -8
  447. package/dist/tools/real-input-attempt.d.ts +32 -0
  448. package/dist/tools/real-input-attempt.js +212 -2
  449. package/dist/tools/reconcile-tools.js +22 -8
  450. package/dist/tools/resolve-target.d.ts +0 -7
  451. package/dist/tools/sdk-marker-probe.d.ts +29 -0
  452. package/dist/tools/sdk-marker-probe.js +57 -0
  453. package/dist/tools/snapshot-delta.d.ts +1 -1
  454. package/dist/tools/tool-kit.d.ts +24 -0
  455. package/dist/tools/tool-kit.js +21 -0
  456. package/dist/tools/tool-names.d.ts +5 -0
  457. package/dist/tools/tool-names.js +5 -0
  458. package/dist/tools/tool-surface.d.ts +85 -6
  459. package/dist/tools/tool-surface.js +122 -2
  460. package/dist/tools/tools-helpers.d.ts +19 -0
  461. package/dist/tools/tools-helpers.js +46 -0
  462. package/dist/tools/tools.d.ts +2 -0
  463. package/dist/tools/tools.js +78 -12
  464. package/dist/tools/unadvertised-help.js +4 -4
  465. package/dist/tools/verify-next-baton.d.ts +53 -0
  466. package/dist/tools/verify-next-baton.js +52 -0
  467. package/dist/update/reticle-deps.d.ts +9 -0
  468. package/dist/update/reticle-deps.js +27 -0
  469. package/dist/update/sdk-sync.d.ts +1 -2
  470. package/dist/update/sdk-sync.js +1 -21
  471. package/dist/update/update-nudge.d.ts +25 -10
  472. package/dist/update/update-nudge.js +32 -3
  473. package/dist/update/updater.d.ts +1 -1
  474. package/dist/version/peer-announce.js +4 -2
  475. package/dist/version/sdk-fix.d.ts +47 -0
  476. package/dist/version/sdk-fix.js +127 -0
  477. package/dist/version/version-nudge.d.ts +14 -0
  478. package/dist/version/version-nudge.js +21 -0
  479. package/dist/version/version-skew.d.ts +38 -3
  480. package/dist/version/version-skew.js +83 -7
  481. package/dist/visual/visual-store.d.ts +6 -6
  482. package/dist/visual/visual-store.js +18 -16
  483. package/dist/visual/visual-tools.js +57 -12
  484. package/docs/README.md +2 -1
  485. package/docs/cli/cloud.mdx +1 -1
  486. package/docs/cli/doctor.mdx +2 -0
  487. package/docs/cli/serve.mdx +4 -2
  488. package/docs/cli/verify.mdx +4 -5
  489. package/docs/cli.mdx +7 -3
  490. package/docs/desktop.mdx +1 -1
  491. package/docs/docs.json +2 -2
  492. package/docs/gates.md +1 -1
  493. package/docs/getting-started.md +19 -3
  494. package/docs/index.mdx +9 -6
  495. package/docs/install-agentic.mdx +38 -12
  496. package/docs/install-manual.mdx +1 -1
  497. package/docs/packages/browser.mdx +2 -2
  498. package/docs/packages/server.mdx +1 -1
  499. package/docs/packages.mdx +8 -6
  500. package/docs/platform-integration.md +17 -0
  501. package/docs/predicates.mdx +8 -0
  502. package/docs/reticle-manual.md +106 -0
  503. package/docs/skill-file.mdx +3 -3
  504. package/docs/telemetry-contract.md +108 -4
  505. package/docs/tools-act-sequence.mdx +16 -0
  506. package/docs/tools-tools-and-run.mdx +2 -4
  507. package/docs/troubleshooting.mdx +9 -0
  508. package/docs/usage.md +4 -3
  509. package/package.json +4 -3
  510. package/dist/init/cursor.d.ts +0 -24
  511. package/dist/init/cursor.js +0 -80
  512. package/dist/session/session-readiness.d.ts +0 -34
  513. package/dist/session/session-readiness.js +0 -36
@@ -113,5 +113,64 @@ export interface ContradictionOptions {
113
113
  * with no view.
114
114
  */
115
115
  renderProved?: boolean | undefined;
116
+ /**
117
+ * The document currently under observation, as the session derived it from its own event stream.
118
+ *
119
+ * Every rule below reasons about "the same window", and a window is scoped by time and by
120
+ * ring-buffer capacity and by nothing else — so it can still hold the traffic of a page a full
121
+ * navigation or a reload has already thrown away. Naming that traffic as the cause of an action
122
+ * taken now is true about the bytes and false about the world.
123
+ *
124
+ * Undefined means nobody could say which document is current (an SDK too old to stamp one, a caller
125
+ * with no session in hand), and the scoping then does nothing at all — `isSameDocument` treats
126
+ * absence as current on both sides, so the engine behaves exactly as it did before this existed.
127
+ */
128
+ currentDocumentId?: string | undefined;
129
+ /**
130
+ * The edit epoch currently in force, as the session derived it from its own event stream.
131
+ *
132
+ * The edit-shaped half of `currentDocumentId`. A hot update replaces modules and re-renders inside
133
+ * the SAME document, so the document id cannot see it and observations of code the agent has
134
+ * already rewritten go on answering for it in silence.
135
+ *
136
+ * Undefined means nobody could say (an SDK too old to stamp one, a page with no hot-update channel,
137
+ * a caller with no session in hand) and the scoping then does nothing at all — `isSameEditEpoch`
138
+ * treats absence as current on both sides.
139
+ */
140
+ currentEditEpoch?: number | undefined;
141
+ /**
142
+ * The page under test, as the session last recorded it — the app's own origin, in URL form.
143
+ *
144
+ * The first-party/third-party axis. Every rule below asks "did the app disagree with itself", and
145
+ * a failed analytics beacon is not the app: reported independently from several apps, any
146
+ * analytics package installed was enough to grade a correct drive `contradicted`, and on one app
147
+ * EVERY assertion came back that way forever, because it fires a branding call on page load. A
148
+ * verdict field that answers "no" to everything has stopped being a verdict field.
149
+ *
150
+ * Third-party traffic is dropped here rather than reported at a lower severity, for the reason the
151
+ * dev-tooling split is: the rules below would each have to learn to say it. The exclusion is never
152
+ * silent — the URLs ride out in the same disclosure line the toolchain's do — and the calls
153
+ * themselves are untouched in `reticle_network` and the event timeline.
154
+ *
155
+ * Undefined disables the axis, exactly as an undefined `currentDocumentId` disables the document
156
+ * one: a caller that cannot say which page is under test gets the behaviour it had before this.
157
+ */
158
+ appOrigin?: string | undefined;
116
159
  }
160
+ /**
161
+ * Cross-channel contradictions in this window, with the edit-epoch caveat attached when it applies.
162
+ *
163
+ * Cross-epoch evidence is LABELLED rather than excluded, which is the opposite of what the document
164
+ * scoping does, and the difference is the point. A navigation is total — it throws away the page,
165
+ * the refs, the in-flight requests and the state — so nothing recorded before it is still about the
166
+ * world, and dropping it is the only honest option. A hot update is not: most modules, most of the
167
+ * DOM, the whole network log and every console line survive one, so most of what was observed a
168
+ * second before an edit is still true a second after. Excluding it would empty windows that hold
169
+ * real findings, and an emptied window reads as "nothing happened" — which is the more expensive of
170
+ * the two wrong answers and the one this whole family of checks exists to prevent.
171
+ *
172
+ * So the findings stand and the caveat is said out loud, and only when it is unambiguous: EVERY
173
+ * observation in the window predates the edit. One post-edit observation and the agent is already
174
+ * looking at the code it wrote, so the label would be noise.
175
+ */
117
176
  export declare function findContradictions(allEvents: readonly ReticleEvent[], options?: ContradictionOptions): Contradiction[];
@@ -1,4 +1,5 @@
1
- import { ContradictionKind, EventType, MUTATING_METHODS, isDevToolingUrl, } from '@reticlehq/core';
1
+ import { ContradictionKind, EventType, MUTATING_METHODS, isDevToolingUrl, isSameDocument, isThirdPartyUrl, isSameEditEpoch, urlForMatch, } from '@reticlehq/core';
2
+ import { describeSuperseded } from './observed-in-window.js';
2
3
  import { findStaleResponses } from './stale-response.js';
3
4
  import { findBodyFailures } from './body-failures.js';
4
5
  import { findEchoMismatches } from './echo-mismatch.js';
@@ -11,6 +12,7 @@ function netCall(e) {
11
12
  return {
12
13
  method: (asString(e.data['method']) ?? '').toUpperCase(),
13
14
  url: asString(e.data['url']) ?? '',
15
+ matchUrl: urlForMatch(e.data),
14
16
  status,
15
17
  // `ok` is authoritative when present (IPC sets it explicitly); status is the HTTP fallback.
16
18
  // Neither present = no verdict was ever reported, which stays undefined all the way through.
@@ -138,6 +140,45 @@ const NET_TYPES = new Set([
138
140
  EventType.NET_REQUEST,
139
141
  EventType.NET_STREAM,
140
142
  ]);
143
+ /**
144
+ * Hash-router paths put the route in the fragment (`#/settings`, `#!/home`). An in-page skip link
145
+ * does not (`#main-content`, `#`, empty). The blank-destination rule must still see the former.
146
+ */
147
+ function isInPageFragment(hash) {
148
+ if ('' === hash || '#' === hash)
149
+ return true;
150
+ const body = hash.startsWith('#') ? hash.slice(1) : hash;
151
+ return !body.startsWith('/') && !body.startsWith('!');
152
+ }
153
+ function hrefAsUrl(value) {
154
+ if (value === undefined || '' === value)
155
+ return undefined;
156
+ try {
157
+ return new URL(value);
158
+ }
159
+ catch {
160
+ return undefined;
161
+ }
162
+ }
163
+ /**
164
+ * Same origin + pathname + search, different in-page fragment — a skip link, not a new view.
165
+ *
166
+ * Returns false when `from`/`to` are missing, so older events without hrefs keep the existing rule.
167
+ */
168
+ function isSameDocumentHashAnchor(event) {
169
+ if (event.type !== EventType.ROUTE_CHANGE)
170
+ return false;
171
+ const from = hrefAsUrl(asString(event.data['from']));
172
+ const to = hrefAsUrl(asString(event.data['to']));
173
+ if (from === undefined || to === undefined)
174
+ return false;
175
+ if (from.origin !== to.origin || from.pathname !== to.pathname || from.search !== to.search) {
176
+ return false;
177
+ }
178
+ if (from.hash === to.hash)
179
+ return false;
180
+ return isInPageFragment(to.hash);
181
+ }
141
182
  /**
142
183
  * Split the window into the app's traffic and the dev toolchain's own (see `DevToolingChannel`).
143
184
  *
@@ -148,13 +189,15 @@ const NET_TYPES = new Set([
148
189
  * every action. The overlay's own 404s and duplicate fetches are the same story on other checks,
149
190
  * which is why the split happens ONCE here rather than in the one check that reported it.
150
191
  */
151
- function splitDevTooling(events) {
192
+ function splitForeignTraffic(events, appOrigin) {
152
193
  const ignored = [];
153
194
  const app = events.filter((e) => {
154
195
  if (!NET_TYPES.has(e.type))
155
196
  return true;
156
197
  const url = asString(e.data['url']);
157
- if (!isDevToolingUrl(url))
198
+ // Somebody else's code, twice over: the toolchain's own channel, and any site that is not the
199
+ // app under test. Neither can answer the question every rule below asks.
200
+ if (!isDevToolingUrl(url) && !isThirdPartyUrl(url, appOrigin))
158
201
  return true;
159
202
  if (url !== undefined && !ignored.includes(url))
160
203
  ignored.push(url);
@@ -162,9 +205,76 @@ function splitDevTooling(events) {
162
205
  });
163
206
  return { app, ignored };
164
207
  }
208
+ /**
209
+ * Cross-channel contradictions in this window, with the edit-epoch caveat attached when it applies.
210
+ *
211
+ * Cross-epoch evidence is LABELLED rather than excluded, which is the opposite of what the document
212
+ * scoping does, and the difference is the point. A navigation is total — it throws away the page,
213
+ * the refs, the in-flight requests and the state — so nothing recorded before it is still about the
214
+ * world, and dropping it is the only honest option. A hot update is not: most modules, most of the
215
+ * DOM, the whole network log and every console line survive one, so most of what was observed a
216
+ * second before an edit is still true a second after. Excluding it would empty windows that hold
217
+ * real findings, and an emptied window reads as "nothing happened" — which is the more expensive of
218
+ * the two wrong answers and the one this whole family of checks exists to prevent.
219
+ *
220
+ * So the findings stand and the caveat is said out loud, and only when it is unambiguous: EVERY
221
+ * observation in the window predates the edit. One post-edit observation and the agent is already
222
+ * looking at the code it wrote, so the label would be noise.
223
+ */
165
224
  export function findContradictions(allEvents, options = {}) {
225
+ const found = findWindowContradictions(allEvents, options);
226
+ const predates = allEvents.length > 0 &&
227
+ allEvents.every((e) => !isSameEditEpoch(e.editEpoch, options.currentEditEpoch));
228
+ if (!predates)
229
+ return found;
230
+ // Prepended, not appended: the caveat governs how everything under it should be read, and it has
231
+ // to survive the rules that return early with a single finding of their own.
232
+ return [
233
+ {
234
+ kind: ContradictionKind.EVIDENCE_PREDATES_EDIT,
235
+ claim: 'these observations describe the code as it is now',
236
+ counter: 'every one of them was recorded before the last hot update landed in the page',
237
+ detail: 'the source changed and the page re-rendered after this evidence was captured, so it describes code that has since been replaced — nothing here is necessarily wrong, but nothing here has seen the edit either. Drive the app again to verify the current code',
238
+ },
239
+ ...found,
240
+ ];
241
+ }
242
+ function findWindowContradictions(allEvents, options) {
166
243
  const found = [];
167
- const { app: events, ignored: ignoredDevTooling } = splitDevTooling(allEvents);
244
+ const { app: allApp, ignored: ignoredForeign } = splitForeignTraffic(allEvents, options.appOrigin);
245
+ // ── Evidence belonging to a document that has since been replaced ───────────────────────────
246
+ // Scoped ONCE, here, for the same reason the dev-tooling split is: every rule below asks "what
247
+ // else was in this window", and answering that with a dead page's traffic is a defect in all of
248
+ // them rather than in whichever one reported it. Applied after the dev-tooling split so the count
249
+ // reported below is the app's own evidence and not the toolchain's noise.
250
+ const scoped = allApp.filter((e) => isSameDocument(e.documentId, options.currentDocumentId));
251
+ const superseded = allApp.length - scoped.length;
252
+ // An empty window was always allowed to mean "nothing happened"; that reading is only unsafe once
253
+ // supersession is what emptied it. Reported ALONE and before every rule below, because a window
254
+ // with nothing left in it is exactly the shape `action-had-no-effect` fires on — so without this
255
+ // the fix would have swapped a wrong citation for a wrong accusation.
256
+ if (superseded > 0 && 0 === scoped.length) {
257
+ return [
258
+ {
259
+ kind: ContradictionKind.EVIDENCE_SUPERSEDED,
260
+ claim: 'this window holds observations that could answer for the action',
261
+ counter: describeSuperseded('observations', superseded),
262
+ detail: 'a full navigation or a reload built a new document, and everything recorded here belongs to the old one — citing it would name requests, errors and state that no longer describe anything on screen',
263
+ },
264
+ ];
265
+ }
266
+ // ── Evidence that predates the action ───────────────────────────────────────────────────────
267
+ // The attribution floor, and it is scoped ONCE here for the same reason the two filters above are:
268
+ // a rule that reads "what else was in this window" and gets handed traffic from before the caller
269
+ // acted is not refining its answer, it is answering about somebody else's action. Three rules
270
+ // (`duplicate-request`, `signal-without-consequence`, `consequence-elsewhere`) each checked this
271
+ // for themselves and the other six swept the whole window — which is how an assert with no `since`
272
+ // came to be judged against everything that had ever happened in the tab.
273
+ //
274
+ // Undefined means nothing attributed the window to an action at all; the floor then does nothing,
275
+ // and the consequence rules below decline to speak instead. See `advanced`.
276
+ const floor = options.actionSince;
277
+ const events = floor === undefined ? scoped : scoped.filter((e) => e.t >= floor);
168
278
  const settled = events.filter((e) => e.type === EventType.NET_REQUEST).map(netCall);
169
279
  // ── Overlapping reads that settled out of order ─────────────────────────────────────────────
170
280
  // Independent of any action, so it runs on every window: the race is a property of the timeline.
@@ -197,7 +307,22 @@ export function findContradictions(allEvents, options = {}) {
197
307
  // The failures NOBODY declared — see ContradictionOptions.expectedFailures. Only the heuristic
198
308
  // "the UI moved while a request failed" rule reads this; the sharp rules still read `failed`.
199
309
  const unexpected = failed.filter((c) => !matchesDeclaredFailure(c, options.expectedFailures ?? []));
200
- const advanced = uiAdvanced(events);
310
+ /**
311
+ * Did the UI move — and is anybody entitled to say so?
312
+ *
313
+ * `undefined` is the third answer and the point of the tri-state: "the UI moved forward while a
314
+ * request failed" is a claim about CAUSATION, and over a window nothing attributed to an action
315
+ * the two halves merely co-occurred. A passive `reticle_assert` performs nothing, so ambient
316
+ * traffic — a poll, a page-load bootstrap, a branding call — is not its consequence and must not
317
+ * decide its verdict.
318
+ *
319
+ * Typed rather than gated with a boolean so it cannot be skipped: every rule that reasons from UI
320
+ * movement has to answer `undefined` explicitly, including one added later. The rules that read
321
+ * the app's OWN claims (a success signal over a failed call) or its payloads (a 2xx whose body
322
+ * says it failed, a field echoed back wrong) are untouched — those are things the app said, not
323
+ * consequences anybody inferred, and they are true whoever caused them.
324
+ */
325
+ const advanced = floor === undefined ? undefined : uiAdvanced(events);
201
326
  const signals = events
202
327
  .filter((e) => e.type === EventType.SIGNAL)
203
328
  .map((e) => asString(e.data['name']) ?? 'signal');
@@ -205,7 +330,13 @@ export function findContradictions(allEvents, options = {}) {
205
330
  // A navigation that neither fetches nor renders arrived nowhere. Distinct from a dead control:
206
331
  // the control worked, the DESTINATION is empty — which is why every "did the click do something"
207
332
  // heuristic passes it, a route change being unambiguously something.
208
- const routed = events.some((e) => e.type === EventType.ROUTE_CHANGE);
333
+ const routeEvents = events.filter((e) => e.type === EventType.ROUTE_CHANGE);
334
+ const routed = routeEvents.length > 0;
335
+ // A skip link (`href="#main-content"`) is a same-document hash change. The observable
336
+ // consequences are location.hash, focus, and scroll — not a DOM mutation. Treating it as a
337
+ // blank destination made "did my skip link work" unanswerable. Hash-router paths (`#/invoices`)
338
+ // still go through the rule: those ARE a new view.
339
+ const hashAnchorOnly = routed && routeEvents.every(isSameDocumentHashAnchor);
209
340
  // `dom.text` counts as rendered, and it has to: React reconciles a destination IN PLACE far more
210
341
  // often than it adds nodes. Measured on three ordinary sidebar navigations of the bench app — every
211
342
  // one emitted { dom.attr:2, dom.text:2, render.commit, state.change } and ZERO dom.added/removed,
@@ -220,7 +351,7 @@ export function findContradictions(allEvents, options = {}) {
220
351
  e.type === EventType.DOM_REMOVED ||
221
352
  e.type === EventType.DOM_TEXT);
222
353
  const fetched = events.some((e) => e.type === EventType.NET_REQUEST || e.type === EventType.NET_PENDING);
223
- if (routed && !rendered && !fetched && true !== options.renderProved) {
354
+ if (routed && !hashAnchorOnly && !rendered && !fetched && true !== options.renderProved) {
224
355
  found.push({
225
356
  kind: ContradictionKind.ROUTE_RENDERED_NOTHING,
226
357
  claim: 'the app navigated to a new route',
@@ -268,60 +399,155 @@ export function findContradictions(allEvents, options = {}) {
268
399
  // Ordering cannot decide this: an optimistic UI legitimately fires its success signal BEFORE the
269
400
  // response, so "the claim must follow the failure" would miss the real defect. What separates them
270
401
  // is not when the app spoke, it is whether it took it back.
271
- if (failed.length > 0 && successSignals.length > 0 && !failureAcknowledged(events)) {
402
+ // WHAT failed decides this, not whether an action was attributed.
403
+ //
404
+ // The claim is a thing the app said, so the attribution floor does not apply to it — the flagship
405
+ // false green is an app asserting success on a PASSIVE assert while its own write failed, and
406
+ // requiring an action would lose exactly that. But the COUNTER was any failure in the window, and
407
+ // that is the same window statement scoped out of every other rule here. Measured: two of the
408
+ // three false positives surviving the first scoping pass were this rule.
409
+ //
410
+ // A success signal claims a CHANGE was made. A failed mutation is evidence against that claim; a
411
+ // failed read is not. Background polls, prefetches and telemetry GETs fail constantly in healthy
412
+ // apps and say nothing about whether a write landed. Structural, not a timing heuristic, and the
413
+ // distinction was already encoded next door in `isMutating`.
414
+ const failedWrites = failed.filter(isMutating);
415
+ // The same scoping, for the same reason, one branch down. "The UI moved forward" is also a claim
416
+ // that something CHANGED, and it was still reading ANY failure — so a first-party poll failing
417
+ // during an action contradicted a verdict the action had genuinely earned. Measured on the
418
+ // observation benchmark: one of two false positives across 47 cells, and the argument for it is
419
+ // the paragraph above, which had simply not been carried down here.
420
+ const unexpectedWrites = unexpected.filter(isMutating);
421
+ if (failedWrites.length > 0 && successSignals.length > 0 && !failureAcknowledged(events)) {
272
422
  found.push({
273
423
  kind: ContradictionKind.SIGNAL_CONTRADICTED,
274
424
  claim: `the app fired ${successSignals.map((s) => `"${s}"`).join(', ')}`,
275
- counter: `${String(failed.length)} request(s) in the same window failed`,
276
- detail: failed.map(describe).join('; '),
425
+ counter: `${String(failedWrites.length)} write(s) in the same window failed`,
426
+ detail: failedWrites.map(describe).join('; '),
277
427
  });
278
428
  }
279
- else if (unexpected.length > 0 && advanced && !misattributed && !failureAcknowledged(events)) {
429
+ else if (unexpectedWrites.length > 0 &&
430
+ true === advanced &&
431
+ !misattributed &&
432
+ !failureAcknowledged(events)) {
280
433
  found.push({
281
434
  kind: ContradictionKind.UI_ADVANCED_REQUEST_FAILED,
282
435
  claim: 'the UI moved forward (DOM/store/route changed)',
283
- counter: `${String(unexpected.length)} request(s) in the same window failed`,
284
- detail: unexpected.map(describe).join('; '),
436
+ counter: `${String(unexpectedWrites.length)} request(s) in the same window failed`,
437
+ detail: unexpectedWrites.map(describe).join('; '),
285
438
  });
286
439
  }
287
440
  // ── A write succeeded and nothing on the client moved ───────────────────────────────────────
288
441
  // Writes only: a GET that changes nothing is a prefetch; a POST that changes nothing is a lost
289
442
  // write, a response parsed into the void, or a render that never happened.
290
- if (!advanced) {
443
+ if (false === advanced) {
291
444
  const ignoredWrites = settled.filter((c) => true === c.ok && isMutating(c));
292
445
  if (ignoredWrites.length > 0) {
293
- found.push({
294
- kind: ContradictionKind.RESPONSE_IGNORED,
295
- claim: `${String(ignoredWrites.length)} write(s) succeeded on the server`,
296
- counter: 'nothing on the client changed — no DOM, store or route movement',
297
- detail: ignoredWrites.map(describe).join('; '),
298
- });
446
+ // ...unless THIS document handed the consequence to another browsing context. An OAuth sign-in
447
+ // posts, succeeds, and continues in a popup the in-page SDK cannot follow (#508): the original
448
+ // tab legitimately never changes, and response-ignored would accuse it of ignoring a response
449
+ // it handed off. The opened-context event flips the reading from "the client did nothing" to
450
+ // "the client went where we cannot look". Scoped like every rule here to the attribution floor
451
+ // (`options.actionSince`; the local below is declared later, for the window rules).
452
+ const contextFloor = options.actionSince;
453
+ const openedContext = events.some((e) => e.type === EventType.CONTEXT_OPENED && contextFloor !== undefined && e.t >= contextFloor);
454
+ found.push(openedContext
455
+ ? {
456
+ kind: ContradictionKind.CONSEQUENCE_ELSEWHERE,
457
+ claim: `${String(ignoredWrites.length)} write(s) succeeded on the server`,
458
+ counter: 'this document never changed because the page opened another browsing context during this window (e.g. an OAuth popup), where the in-page SDK cannot observe the result',
459
+ detail: ignoredWrites.map(describe).join('; '),
460
+ }
461
+ : {
462
+ kind: ContradictionKind.RESPONSE_IGNORED,
463
+ claim: `${String(ignoredWrites.length)} write(s) succeeded on the server`,
464
+ counter: 'nothing on the client changed — no DOM, store or route movement',
465
+ detail: ignoredWrites.map(describe).join('; '),
466
+ });
299
467
  }
300
468
  }
469
+ // ── The app announced a consequence and nothing else moved ─────────────────────────────────
470
+ // Scoped as tightly as the evidence allows, because this rule fires on the ABSENCE of everything
471
+ // else and that is the easiest way to build a false positive.
472
+ //
473
+ // Only when the window is otherwise EMPTY: no DOM, no store, no route (`!advanced`) and no request
474
+ // at all. A request means the app reached for something, and whether it settled, failed or was
475
+ // ignored belongs to three other rules — firing here too would report one fact twice.
476
+ //
477
+ // `successSignals` reuses the failure-shaped filter above: an app that announced `deploy:failed`
478
+ // is correctly reporting that nothing happened, and accusing it inverts the meaning of the one app
479
+ // doing this right.
480
+ //
481
+ // And only for a window attributed to an ACTION. `reticle_assert` OBSERVES — there is no click
482
+ // whose consequence should have corroborated anything, so an assert over a quiet window carrying
483
+ // one signal is an ordinary read, not a claim nothing backs. Without this the rule reddened eight
484
+ // existing tests that assert exactly that, which is the false-positive class this scoping exists
485
+ // to prevent.
486
+ if (options.actionSince !== undefined &&
487
+ false === advanced &&
488
+ 0 === settled.length &&
489
+ successSignals.length > 0) {
490
+ found.push({
491
+ kind: ContradictionKind.SIGNAL_WITHOUT_CONSEQUENCE,
492
+ claim: `the app fired ${successSignals.map((s) => `"${s}"`).join(', ')}`,
493
+ counter: 'nothing else in the window moved — no DOM, no store, no route, no request — so the only ' +
494
+ 'evidence that anything happened is the app saying so',
495
+ detail: 'a signal emitted from the value the app was ASKED for, rather than the one it committed, ' +
496
+ 'reads identically to one that worked',
497
+ });
498
+ }
301
499
  // ── The same write fired more than once, inside ONE action's window ─────────────────────────
302
500
  // Attribution is the whole rule here, not a refinement of it: counting `method + url` over whatever
303
501
  // window the caller handed in turns two legitimate separate saves into a double submit. See
304
502
  // ContradictionOptions.actionSince.
503
+ //
504
+ // A captured request body joins the identity. A command-bus API posts every mutation to one URL
505
+ // and discriminates on a body field (`{"command":"study.stage.set"}` vs `{"command":"mesh.plan"}`),
506
+ // so under URL alone each of those distinct writes read as a repeat of the first, and clean
507
+ // verdicts degraded to unknown behind doubles that were never doubles. When no body was captured
508
+ // (capture off, or a non-text body, which reports a type marker instead) there is nothing to
509
+ // compare and the URL alone stands, as before; a window where only some calls carried bodies
510
+ // compares nothing rather than guess.
305
511
  const actionSince = options.actionSince;
306
512
  if (actionSince !== undefined) {
513
+ /**
514
+ * When the window navigated, and therefore when a write stops belonging to the user's action.
515
+ *
516
+ * React StrictMode double-invokes a mount effect in development, so clicking a nav link lands
517
+ * two identical writes inside the action's own window. Nothing scoped them out and they read as
518
+ * a double submit — measured on the observation benchmark as one of two false positives.
519
+ *
520
+ * The route change is the structural tell, not a heuristic: the claim this rule makes is "one
521
+ * user action was performed", and writes that follow a navigation belong to the mount of the
522
+ * view navigated TO. A real double submit fires from the view it is already on, with nothing in
523
+ * between — and one that navigates AFTER submitting is still counted, because the order is what
524
+ * distinguishes them.
525
+ */
526
+ const navigatedAt = events.find((e) => EventType.ROUTE_CHANGE === e.type && e.t >= actionSince)?.t;
307
527
  const writeCounts = new Map();
308
528
  for (const event of events) {
309
529
  if (event.type !== EventType.NET_REQUEST || event.t < actionSince)
310
530
  continue;
531
+ if (navigatedAt !== undefined && event.t >= navigatedAt)
532
+ continue;
311
533
  const call = netCall(event);
312
534
  if (!isMutating(call))
313
535
  continue;
314
- const key = `${call.method} ${call.url}`;
315
- writeCounts.set(key, (writeCounts.get(key) ?? 0) + 1);
536
+ const label = `${call.method} ${call.url}`;
537
+ const body = asString(event.data['requestBody']);
538
+ const key = body === undefined || 0 === body.length ? label : `${label} ${body}`;
539
+ const entry = writeCounts.get(key) ?? { label, count: 0 };
540
+ entry.count += 1;
541
+ writeCounts.set(key, entry);
316
542
  }
317
- for (const [key, count] of writeCounts) {
543
+ for (const [, { label, count }] of writeCounts) {
318
544
  if (count < 2)
319
545
  continue;
320
546
  found.push({
321
547
  kind: ContradictionKind.DUPLICATE_REQUEST,
322
548
  claim: 'one user action was performed',
323
549
  counter: `the same write fired ${String(count)} times`,
324
- detail: `${key} ×${String(count)}`,
550
+ detail: `${label} ×${String(count)}`,
325
551
  });
326
552
  }
327
553
  }
@@ -329,7 +555,7 @@ export function findContradictions(allEvents, options = {}) {
329
555
  // Gated on the UI having moved: an in-flight request while the app is still visibly waiting is
330
556
  // just a slow request, not a contradiction. It becomes one when the app proceeded regardless —
331
557
  // which is also what makes a later `{ kind: "settled" }` assertion a false green.
332
- if (advanced) {
558
+ if (true === advanced) {
333
559
  const settledIds = new Set(events
334
560
  .filter((e) => e.type === EventType.NET_REQUEST)
335
561
  .map((e) => asString(e.data['id']))
@@ -347,9 +573,9 @@ export function findContradictions(allEvents, options = {}) {
347
573
  // the finding says which URLs, so an agent reading it can see what Reticle chose to ignore.
348
574
  detail: [
349
575
  inFlight.map((p) => describe(p.call)).join('; '),
350
- ...(0 === ignoredDevTooling.length
576
+ ...(0 === ignoredForeign.length
351
577
  ? []
352
- : [`ignored as dev tooling: ${ignoredDevTooling.join(', ')}`]),
578
+ : [`ignored as dev tooling or third-party: ${ignoredForeign.join(', ')}`]),
353
579
  ].join(' — '),
354
580
  });
355
581
  }
@@ -31,6 +31,22 @@ export interface DeclaredExpectations {
31
31
  rendersContent: boolean;
32
32
  }
33
33
  export declare function declaredExpectations(predicate: Predicate | undefined): DeclaredExpectations;
34
+ /**
35
+ * Did the caller name a consequence that does not depend on the response body?
36
+ *
37
+ * The unread-body clause exists for the case where the body is the ONLY channel that could have
38
+ * contradicted the screen — a 200 with per-item failures inside, a GraphQL error that is also a
39
+ * 200. A declared string on screen, store path, signal, route, or an element located by role /
40
+ * name / testid that held is a different channel, and grading `unknown` there costs a real
41
+ * verdict (measured: a 201 plus the unique row, unread body, agent went to enable capture instead
42
+ * of finishing the drive).
43
+ *
44
+ * Same conservatism as `declaredExpectations`: only the top level and `allOf`. An `anyOf` branch
45
+ * may never have held, and honouring it would skip the unread caveat on the strength of a net
46
+ * success that was the branch that actually matched. An `absent` element is not a consequence the
47
+ * body cannot own.
48
+ */
49
+ export declare function declaresBodyIndependentChannel(predicate: Predicate | undefined): boolean;
34
50
  /**
35
51
  * Does a call the window recorded match a failure the caller declared?
36
52
  *
@@ -41,5 +57,6 @@ export declare function declaredExpectations(predicate: Predicate | undefined):
41
57
  export declare function matchesDeclaredFailure(call: {
42
58
  method: string;
43
59
  url: string;
60
+ matchUrl?: string;
44
61
  status: number | undefined;
45
62
  }, declared: readonly DeclaredNetFailure[]): boolean;
@@ -53,6 +53,47 @@ export function declaredExpectations(predicate) {
53
53
  walk(predicate);
54
54
  return { netFailures, rendersContent };
55
55
  }
56
+ /**
57
+ * Did the caller name a consequence that does not depend on the response body?
58
+ *
59
+ * The unread-body clause exists for the case where the body is the ONLY channel that could have
60
+ * contradicted the screen — a 200 with per-item failures inside, a GraphQL error that is also a
61
+ * 200. A declared string on screen, store path, signal, route, or an element located by role /
62
+ * name / testid that held is a different channel, and grading `unknown` there costs a real
63
+ * verdict (measured: a 201 plus the unique row, unread body, agent went to enable capture instead
64
+ * of finishing the drive).
65
+ *
66
+ * Same conservatism as `declaredExpectations`: only the top level and `allOf`. An `anyOf` branch
67
+ * may never have held, and honouring it would skip the unread caveat on the strength of a net
68
+ * success that was the branch that actually matched. An `absent` element is not a consequence the
69
+ * body cannot own.
70
+ */
71
+ export function declaresBodyIndependentChannel(predicate) {
72
+ if (predicate === undefined)
73
+ return false;
74
+ const walk = (p) => {
75
+ switch (p.kind) {
76
+ case PredicateKind.ALL_OF:
77
+ return p.predicates.some(walk);
78
+ case PredicateKind.TEXT:
79
+ return true !== p.absent;
80
+ case PredicateKind.SIGNAL:
81
+ case PredicateKind.STATE:
82
+ case PredicateKind.ROUTE:
83
+ return true;
84
+ case PredicateKind.ELEMENT:
85
+ return (true !== p.absent &&
86
+ (p.query.value !== undefined ||
87
+ p.query.text !== undefined ||
88
+ p.query.role !== undefined ||
89
+ p.query.name !== undefined ||
90
+ p.query.testid !== undefined));
91
+ default:
92
+ return false;
93
+ }
94
+ };
95
+ return walk(predicate);
96
+ }
56
97
  /**
57
98
  * Does a call the window recorded match a failure the caller declared?
58
99
  *
@@ -64,7 +105,8 @@ export function matchesDeclaredFailure(call, declared) {
64
105
  return declared.some((d) => {
65
106
  if (d.method !== undefined && d.method.toUpperCase() !== call.method.toUpperCase())
66
107
  return false;
67
- if (d.urlContains !== undefined && !call.url.includes(d.urlContains))
108
+ const haystack = call.matchUrl ?? call.url;
109
+ if (d.urlContains !== undefined && !haystack.includes(d.urlContains))
68
110
  return false;
69
111
  if (d.status !== undefined && d.status !== call.status)
70
112
  return false;
@@ -28,7 +28,15 @@ import { ContradictionKind, EventType, MUTATING_METHODS } from '@reticlehq/core'
28
28
  * - values are compared NORMALISED (trimmed, case-folded, numbers as numbers), so `FR` vs `fr` and
29
29
  * `1` vs `1.0` stay silent while `fr` vs `en` speaks;
30
30
  * - if the key appears anywhere in the response carrying the requested value, it is treated as
31
- * applied — an envelope that echoes both the old and the new value is not a dropped write.
31
+ * applied — an envelope that echoes both the old and the new value is not a dropped write;
32
+ * - the response must look like a restatement of the request before any key is compared. Reported
33
+ * from the field: a command bus POSTed `{command:'chat.send', ...}` and the server answered with
34
+ * the current viewer snapshot. The message arrived over the socket and rendered. `id` in the
35
+ * snapshot meant the viewer, `id` in the request meant the message, and this kind fired. A
36
+ * snapshot that shares a key name is not an echo. A discriminator the request carries that the
37
+ * response does not, or a fat body that shares one coincidental key, is skipped. The existing
38
+ * single-value / not-echoed-at-all guards still trade false negatives away — this narrows on
39
+ * shape, not on value types.
32
40
  *
33
41
  * The residue after those filters is a field the caller set, the server echoed back, and the echoed
34
42
  * value is a genuinely different value. That is worth one line of an agent's attention.
@@ -94,6 +102,60 @@ function scalarsByKey(body, into = new Map(), depth = 0) {
94
102
  }
95
103
  const OK_MIN = 200;
96
104
  const OK_MAX = 300;
105
+ /**
106
+ * Body keys that name the operation rather than a field being persisted. A command bus puts one of
107
+ * these on the request; a snapshot of the current viewer does not echo it. Their absence on the
108
+ * response is the cheapest honest signal that the body is not a restatement of the write.
109
+ */
110
+ const EchoDiscriminator = {
111
+ COMMAND: 'command',
112
+ ACTION: 'action',
113
+ OP: 'op',
114
+ PROCEDURE: 'procedure',
115
+ };
116
+ /**
117
+ * A response carrying this many times more scalar keys than the request, overlapping on fewer than
118
+ * `MIN_SHARED_KEYS`, is a snapshot that happened to reuse a name — not an echo of the write.
119
+ */
120
+ const SNAPSHOT_KEY_RATIO = 3;
121
+ const MIN_SHARED_KEYS = 2;
122
+ /**
123
+ * True when the response looks like a restatement of the request, so overlapping key names can be
124
+ * read as an echo rather than a coincidence.
125
+ *
126
+ * Half the request's comparable keys must appear in the response. That keeps a partial echo
127
+ * (`{density, locale}` answered with only `locale`) in scope, and drops a three-field command whose
128
+ * snapshot shares one name. A request that names an operation the response does not repeat is not
129
+ * an echo at all, regardless of any other overlap.
130
+ */
131
+ function responseRestatesRequest(asked, echoed) {
132
+ const comparable = [];
133
+ for (const [key, wanted] of asked) {
134
+ if (1 === wanted.size)
135
+ comparable.push(key);
136
+ }
137
+ if (0 === comparable.length)
138
+ return false;
139
+ let present = 0;
140
+ for (const key of comparable) {
141
+ if (echoed.has(key))
142
+ present += 1;
143
+ }
144
+ let requestHasDiscriminator = false;
145
+ let responseHasDiscriminator = false;
146
+ for (const key of Object.values(EchoDiscriminator)) {
147
+ if (asked.has(key))
148
+ requestHasDiscriminator = true;
149
+ if (echoed.has(key))
150
+ responseHasDiscriminator = true;
151
+ }
152
+ if (requestHasDiscriminator && !responseHasDiscriminator)
153
+ return false;
154
+ if (echoed.size > comparable.length * SNAPSHOT_KEY_RATIO && present < MIN_SHARED_KEYS) {
155
+ return false;
156
+ }
157
+ return present * 2 >= comparable.length;
158
+ }
97
159
  /**
98
160
  * Contradictions for writes whose response echoes a different value than the request asked for.
99
161
  *
@@ -133,6 +195,8 @@ export function findEchoMismatches(events, actionSince) {
133
195
  continue;
134
196
  const echoed = scalarsByKey(response);
135
197
  const asked = scalarsByKey(request);
198
+ if (!responseRestatesRequest(asked, echoed))
199
+ continue;
136
200
  const dropped = [];
137
201
  for (const [key, wanted] of asked) {
138
202
  // More than one requested value for a key (a before/after pair, a list of items) makes "what
@@ -32,6 +32,14 @@ export declare function matchNet(e: ReticleEvent, method: string | undefined, ur
32
32
  * pending: true }` so a numeric `status` filter excludes them but `urlContains`/`method` match.
33
33
  */
34
34
  export declare function reconcileNet(events: ReticleEvent[]): ReticleEvent[];
35
+ /**
36
+ * Strip the grader-only raw URL before an event is rendered to the agent.
37
+ *
38
+ * `urlRaw` exists so `urlContains` can match a path redaction rewrote. It is a secret-bearing field
39
+ * by construction (the whole reason `url` was redacted) and must never appear in observe, evidence,
40
+ * or a network listing.
41
+ */
42
+ export declare function withoutUrlRaw(event: unknown): unknown;
35
43
  /** Compact network-call summary for reticle_network output — drops event plumbing (t, type,
36
44
  * sessionId, ref, id, initiator, ok) the agent never needs, keeping only method/url/status/ms.
37
45
  * This is the bulk of the token cost: raw ReticleEvent objects are ~5x larger than this. */