@reticlehq/server 2.4.0 → 2.5.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 (416) hide show
  1. package/SKILL.md +702 -0
  2. package/dist/bridge/auth-failure-reason.d.ts +19 -0
  3. package/dist/bridge/auth-failure-reason.js +29 -0
  4. package/dist/{bridge-security.d.ts → bridge/bridge-security.d.ts} +1 -1
  5. package/dist/{bridge-security.js → bridge/bridge-security.js} +1 -1
  6. package/dist/{bridge.d.ts → bridge/bridge.d.ts} +1 -1
  7. package/dist/{bridge.js → bridge/bridge.js} +63 -13
  8. package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +1 -11
  9. package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +9 -9
  10. package/dist/{pairing-token.js → bridge/pairing-token.js} +1 -1
  11. package/dist/capsule/capsule-store.d.ts +8 -3
  12. package/dist/capsule/capsule-store.js +7 -2
  13. package/dist/capsule/capsule.js +3 -3
  14. package/dist/capsule/causal-summary.d.ts +8 -0
  15. package/dist/capsule/causal-summary.js +22 -12
  16. package/dist/capsule/divergence.d.ts +9 -4
  17. package/dist/capsule/divergence.js +8 -8
  18. package/dist/capsule/predicate-to-links.js +11 -8
  19. package/dist/{cli-doctor.js → cli/cli-doctor.js} +9 -2
  20. package/dist/{cli-flow-commands.d.ts → cli/cli-flow-commands.d.ts} +2 -2
  21. package/dist/{cli-flow-commands.js → cli/cli-flow-commands.js} +15 -15
  22. package/dist/{cli-launch.d.ts → cli/cli-launch.d.ts} +32 -3
  23. package/dist/cli/cli-launch.js +171 -0
  24. package/dist/{cli-parse.d.ts → cli/cli-parse.d.ts} +2 -11
  25. package/dist/{cli-parse.js → cli/cli-parse.js} +113 -60
  26. package/dist/{cli-port.d.ts → cli/cli-port.d.ts} +12 -0
  27. package/dist/{cli-port.js → cli/cli-port.js} +9 -9
  28. package/dist/cli/cli-update-commands.d.ts +4 -0
  29. package/dist/cli/cli-update-commands.js +93 -0
  30. package/dist/{cli-verify.js → cli/cli-verify.js} +14 -14
  31. package/dist/{cloud-cli.js → cli/cloud-cli.js} +29 -24
  32. package/dist/cli.d.ts +2 -2
  33. package/dist/cli.js +174 -92
  34. package/dist/cloud/cloud-config.d.ts +0 -1
  35. package/dist/cloud/cloud-config.js +9 -9
  36. package/dist/cloud/cloud-sync.d.ts +1 -7
  37. package/dist/cloud/cloud-sync.js +12 -12
  38. package/dist/crawl/crawl-empty.d.ts +16 -0
  39. package/dist/crawl/crawl-empty.js +24 -0
  40. package/dist/crawl/crawl-tools.js +4 -1
  41. package/dist/crawl/crawl.js +26 -7
  42. package/dist/daemon/daemon-resilience.d.ts +49 -0
  43. package/dist/daemon/daemon-resilience.js +191 -0
  44. package/dist/daemon/daemon-usefulness.d.ts +61 -0
  45. package/dist/daemon/daemon-usefulness.js +62 -0
  46. package/dist/{daemon.d.ts → daemon/daemon.d.ts} +33 -8
  47. package/dist/{daemon.js → daemon/daemon.js} +80 -48
  48. package/dist/daemon/idle-grace.d.ts +32 -0
  49. package/dist/daemon/idle-grace.js +41 -0
  50. package/dist/{idle-shutdown.d.ts → daemon/idle-shutdown.d.ts} +13 -0
  51. package/dist/{idle-shutdown.js → daemon/idle-shutdown.js} +27 -4
  52. package/dist/domain/domain-model.js +2 -2
  53. package/dist/events/body-failures.js +8 -8
  54. package/dist/events/contradictions.d.ts +1 -1
  55. package/dist/events/contradictions.js +60 -10
  56. package/dist/events/echo-mismatch.js +10 -10
  57. package/dist/events/event-filters.js +6 -6
  58. package/dist/events/json-salvage.d.ts +2 -1
  59. package/dist/events/json-salvage.js +6 -6
  60. package/dist/events/observed-in-window.d.ts +12 -0
  61. package/dist/events/observed-in-window.js +21 -0
  62. package/dist/events/predicate-eval.d.ts +43 -20
  63. package/dist/events/predicate-eval.js +159 -55
  64. package/dist/events/predicate.d.ts +2 -2
  65. package/dist/events/predicate.js +112 -35
  66. package/dist/events/reconcile.js +5 -5
  67. package/dist/events/ring-buffer.js +1 -1
  68. package/dist/events/stale-response.js +6 -6
  69. package/dist/events/testid-near-miss.d.ts +22 -0
  70. package/dist/events/testid-near-miss.js +33 -0
  71. package/dist/events/unit-mismatch.js +8 -8
  72. package/dist/flows/affected-tools.js +19 -4
  73. package/dist/flows/affected.js +1 -1
  74. package/dist/flows/annotate-target.d.ts +1 -0
  75. package/dist/flows/annotate-target.js +20 -0
  76. package/dist/flows/annotate-tools.js +25 -3
  77. package/dist/flows/annotate.js +17 -4
  78. package/dist/flows/assertion-integrity.d.ts +2 -1
  79. package/dist/flows/assertion-tiers-store.d.ts +3 -66
  80. package/dist/flows/assertion-tiers-store.js +2 -2
  81. package/dist/flows/attributed-failure.d.ts +17 -0
  82. package/dist/flows/attributed-failure.js +20 -0
  83. package/dist/flows/buddy-status.d.ts +2 -1
  84. package/dist/flows/buddy-status.js +2 -2
  85. package/dist/flows/coverage.d.ts +3 -2
  86. package/dist/flows/coverage.js +2 -2
  87. package/dist/flows/decision.js +24 -9
  88. package/dist/flows/empty-flow.d.ts +6 -0
  89. package/dist/flows/empty-flow.js +24 -0
  90. package/dist/flows/flake-store.d.ts +0 -1
  91. package/dist/flows/flake-store.js +1 -1
  92. package/dist/flows/flake.d.ts +2 -1
  93. package/dist/flows/flake.js +2 -2
  94. package/dist/flows/flow-classify.js +12 -0
  95. package/dist/flows/flow-replay-run.js +4 -4
  96. package/dist/flows/flow-replay.d.ts +5 -7
  97. package/dist/flows/flow-replay.js +114 -47
  98. package/dist/flows/flow-report.js +1 -1
  99. package/dist/flows/flow-scope.d.ts +1 -6
  100. package/dist/flows/flow-step-runners.d.ts +26 -6
  101. package/dist/flows/flow-step-runners.js +157 -6
  102. package/dist/flows/flow-success.js +20 -15
  103. package/dist/flows/flow-tools.js +53 -7
  104. package/dist/flows/flows.js +15 -11
  105. package/dist/flows/gate.d.ts +4 -3
  106. package/dist/flows/gate.js +1 -1
  107. package/dist/flows/heal-run.d.ts +0 -8
  108. package/dist/flows/heal-run.js +5 -5
  109. package/dist/flows/heal.js +3 -3
  110. package/dist/flows/predicate-to-expect.d.ts +38 -0
  111. package/dist/flows/predicate-to-expect.js +95 -0
  112. package/dist/flows/replay.d.ts +16 -1
  113. package/dist/flows/replay.js +58 -22
  114. package/dist/flows/role-anchor-nearest.d.ts +6 -0
  115. package/dist/flows/role-anchor-nearest.js +45 -0
  116. package/dist/flows/server-verify.d.ts +1 -1
  117. package/dist/flows/server-verify.js +5 -5
  118. package/dist/flows/verify-change-tools.js +32 -10
  119. package/dist/flows/verify-change-verdict.d.ts +15 -0
  120. package/dist/flows/verify-change-verdict.js +25 -0
  121. package/dist/flows/watch-batcher.d.ts +3 -2
  122. package/dist/honesty/already-true.d.ts +2 -0
  123. package/dist/honesty/already-true.js +31 -0
  124. package/dist/honesty/blind-spots.d.ts +16 -3
  125. package/dist/honesty/blind-spots.js +24 -12
  126. package/dist/honesty/honesty.d.ts +3 -2
  127. package/dist/honesty/honesty.js +4 -4
  128. package/dist/honesty/uncaptured-bodies.js +1 -1
  129. package/dist/honesty/unread-outcome.js +2 -2
  130. package/dist/honesty/verified.d.ts +22 -3
  131. package/dist/honesty/verified.js +48 -8
  132. package/dist/http-server.js +14 -7
  133. package/dist/hunt/hunt-report.d.ts +2 -1
  134. package/dist/hunt/hunt-report.js +2 -2
  135. package/dist/index.d.ts +8 -6
  136. package/dist/index.js +32 -25
  137. package/dist/init/agent-rules.d.ts +2 -1
  138. package/dist/init/agent-rules.js +45 -7
  139. package/dist/init/app-choice.d.ts +20 -0
  140. package/dist/init/app-choice.js +25 -0
  141. package/dist/init/astro-host.d.ts +21 -0
  142. package/dist/init/astro-host.js +27 -0
  143. package/dist/init/astro-patch.d.ts +16 -0
  144. package/dist/init/astro-patch.js +240 -0
  145. package/dist/init/capabilities.js +4 -1
  146. package/dist/init/cra.d.ts +37 -0
  147. package/dist/init/cra.js +90 -0
  148. package/dist/init/cursor.js +39 -4
  149. package/dist/init/desktop-doctor.d.ts +3 -2
  150. package/dist/init/desktop-doctor.js +4 -4
  151. package/dist/init/detect.d.ts +3 -2
  152. package/dist/init/detect.js +16 -5
  153. package/dist/init/mcp-registered.d.ts +12 -0
  154. package/dist/init/mcp-registered.js +14 -0
  155. package/dist/init/mcp.d.ts +27 -2
  156. package/dist/init/mcp.js +40 -4
  157. package/dist/init/next-patch.d.ts +2 -4
  158. package/dist/init/next-patch.js +109 -19
  159. package/dist/init/node-io.js +23 -6
  160. package/dist/init/plan-framework.d.ts +16 -1
  161. package/dist/init/plan-framework.js +132 -8
  162. package/dist/init/plan.d.ts +73 -8
  163. package/dist/init/plan.js +133 -34
  164. package/dist/init/project-id.d.ts +5 -9
  165. package/dist/init/project-id.js +10 -22
  166. package/dist/init/run.d.ts +5 -0
  167. package/dist/init/run.js +210 -36
  168. package/dist/init/slash-command.d.ts +9 -0
  169. package/dist/init/slash-command.js +10 -1
  170. package/dist/init/snippets.d.ts +7 -10
  171. package/dist/init/snippets.js +44 -15
  172. package/dist/init/vite-config.js +1 -1
  173. package/dist/init/workspace-apps.d.ts +23 -0
  174. package/dist/init/workspace-apps.js +68 -0
  175. package/dist/input/driven-redaction.js +1 -1
  176. package/dist/input/network-detail.js +8 -8
  177. package/dist/input/network-mock-tools.js +2 -2
  178. package/dist/input/network-mock.js +4 -4
  179. package/dist/input/real-input.js +8 -3
  180. package/dist/input/scroll-find.js +2 -2
  181. package/dist/input/scroll-tools.d.ts +0 -5
  182. package/dist/input/scroll-tools.js +11 -4
  183. package/dist/input/viewport-tools.js +2 -2
  184. package/dist/journal/ambient-store.d.ts +0 -2
  185. package/dist/journal/ambient-store.js +1 -1
  186. package/dist/journal/ambient.js +1 -1
  187. package/dist/journal/attach-journal.d.ts +2 -1
  188. package/dist/journal/deviation-report.js +6 -6
  189. package/dist/journal/deviation-service.js +1 -1
  190. package/dist/journal/envelope-store.d.ts +0 -2
  191. package/dist/journal/envelope-store.js +1 -1
  192. package/dist/journal/envelope.d.ts +2 -3
  193. package/dist/journal/envelope.js +3 -3
  194. package/dist/journal/journal-query.js +1 -1
  195. package/dist/journal/journal-recorder.d.ts +2 -1
  196. package/dist/journal/journal-recorder.js +2 -2
  197. package/dist/journal/rollups.js +9 -9
  198. package/dist/journal/session-end.d.ts +2 -1
  199. package/dist/journal/session-journal.js +4 -4
  200. package/dist/license/license.d.ts +0 -14
  201. package/dist/license/license.js +8 -8
  202. package/dist/log.d.ts +10 -0
  203. package/dist/log.js +11 -1
  204. package/dist/mcp/mcp-is-error.d.ts +13 -0
  205. package/dist/mcp/mcp-is-error.js +18 -0
  206. package/dist/mcp/mcp-outage.d.ts +31 -0
  207. package/dist/mcp/mcp-outage.js +48 -0
  208. package/dist/mcp/mcp-proxy.d.ts +124 -0
  209. package/dist/mcp/mcp-proxy.js +697 -0
  210. package/dist/{mcp.d.ts → mcp/mcp.d.ts} +13 -10
  211. package/dist/{mcp.js → mcp/mcp.js} +110 -35
  212. package/dist/mcp/proxy-handshake.d.ts +3 -0
  213. package/dist/mcp/proxy-handshake.js +54 -0
  214. package/dist/mcp/proxy-lifecycle.d.ts +64 -0
  215. package/dist/mcp/proxy-lifecycle.js +70 -0
  216. package/dist/mcp/tool-catalog-cache.d.ts +29 -0
  217. package/dist/mcp/tool-catalog-cache.js +55 -0
  218. package/dist/oracles/propose-consequences.d.ts +2 -1
  219. package/dist/oracles/propose-consequences.js +16 -11
  220. package/dist/oracles/self-instrument.d.ts +2 -1
  221. package/dist/oracles/self-instrument.js +1 -1
  222. package/dist/phenomena/phenomena.js +3 -3
  223. package/dist/platform.d.ts +16 -0
  224. package/dist/platform.js +16 -0
  225. package/dist/pool/playwright-launcher.d.ts +15 -2
  226. package/dist/pool/playwright-launcher.js +21 -2
  227. package/dist/project/fs-port.js +1 -1
  228. package/dist/project/project-tools.js +2 -2
  229. package/dist/runs/build-verification-run.js +1 -1
  230. package/dist/runs/repair-prompt.d.ts +0 -6
  231. package/dist/runs/repair-prompt.js +3 -3
  232. package/dist/runs/risk-classify.js +1 -1
  233. package/dist/runs/run-diff.d.ts +0 -2
  234. package/dist/runs/run-diff.js +3 -3
  235. package/dist/runs/run-tools.js +2 -2
  236. package/dist/runs/verification-sync.js +2 -2
  237. package/dist/runs/verify-http.js +1 -1
  238. package/dist/runs/verify-server.js +1 -1
  239. package/dist/session/command-timeout.d.ts +11 -3
  240. package/dist/session/command-timeout.js +2 -2
  241. package/dist/session/control-envelope.d.ts +6 -0
  242. package/dist/session/control-envelope.js +7 -1
  243. package/dist/session/dev-server-probe.d.ts +13 -0
  244. package/dist/session/dev-server-probe.js +52 -0
  245. package/dist/session/last-act.d.ts +17 -3
  246. package/dist/session/last-act.js +16 -4
  247. package/dist/session/live-control-tools.js +7 -2
  248. package/dist/session/live-control.js +1 -1
  249. package/dist/session/looks-like-dev-server.d.ts +14 -0
  250. package/dist/session/looks-like-dev-server.js +25 -0
  251. package/dist/session/no-session-diagnosis.d.ts +38 -0
  252. package/dist/session/no-session-diagnosis.js +70 -0
  253. package/dist/session/no-session-watch.d.ts +19 -0
  254. package/dist/session/no-session-watch.js +61 -0
  255. package/dist/session/observed-state.d.ts +4 -0
  256. package/dist/session/observed-state.js +19 -1
  257. package/dist/session/output-budget.js +1 -1
  258. package/dist/session/session-health.d.ts +8 -1
  259. package/dist/session/session-health.js +8 -7
  260. package/dist/session/session-info.d.ts +26 -7
  261. package/dist/session/session-info.js +42 -1
  262. package/dist/session/session-lease.d.ts +0 -11
  263. package/dist/session/session-lease.js +1 -1
  264. package/dist/session/session-manager.d.ts +3 -0
  265. package/dist/session/session-manager.js +52 -9
  266. package/dist/session/session-readiness.d.ts +1 -1
  267. package/dist/session/session-readiness.js +6 -1
  268. package/dist/session/session-reaper.js +1 -1
  269. package/dist/session/session-reconnect.d.ts +32 -0
  270. package/dist/session/session-reconnect.js +42 -0
  271. package/dist/session/session.d.ts +14 -2
  272. package/dist/session/session.js +69 -26
  273. package/dist/session/state-select.d.ts +1 -1
  274. package/dist/session/state-select.js +1 -1
  275. package/dist/status-payload.d.ts +19 -0
  276. package/dist/status-payload.js +11 -0
  277. package/dist/telemetry/argument-shape.js +10 -1
  278. package/dist/telemetry/browser-mode.d.ts +27 -0
  279. package/dist/telemetry/browser-mode.js +33 -0
  280. package/dist/telemetry/bug-found.d.ts +17 -21
  281. package/dist/telemetry/bug-found.js +120 -8
  282. package/dist/telemetry/cli-telemetry.d.ts +2 -12
  283. package/dist/telemetry/cli-telemetry.js +27 -4
  284. package/dist/telemetry/daemon-telemetry.d.ts +19 -0
  285. package/dist/telemetry/daemon-telemetry.js +12 -2
  286. package/dist/telemetry/dev-repo.js +4 -4
  287. package/dist/telemetry/error-fingerprint.js +21 -2
  288. package/dist/telemetry/feedback-cli.d.ts +1 -1
  289. package/dist/telemetry/feedback-cli.js +4 -4
  290. package/dist/telemetry/feedback-context.d.ts +22 -0
  291. package/dist/telemetry/feedback-context.js +41 -1
  292. package/dist/telemetry/feedback-delivery.d.ts +20 -0
  293. package/dist/telemetry/feedback-delivery.js +30 -0
  294. package/dist/telemetry/feedback-outbox.d.ts +22 -0
  295. package/dist/telemetry/feedback-outbox.js +87 -0
  296. package/dist/telemetry/feedback.d.ts +22 -1
  297. package/dist/telemetry/feedback.js +67 -8
  298. package/dist/telemetry/git-facts.js +1 -1
  299. package/dist/telemetry/session-metrics.d.ts +2 -0
  300. package/dist/telemetry/session-metrics.js +68 -6
  301. package/dist/telemetry/telemetry.d.ts +27 -2
  302. package/dist/telemetry/telemetry.js +119 -29
  303. package/dist/telemetry/verification-of.d.ts +22 -0
  304. package/dist/telemetry/verification-of.js +84 -0
  305. package/dist/tools/act-capsule.js +2 -2
  306. package/dist/tools/act-danger.d.ts +12 -0
  307. package/dist/tools/act-danger.js +20 -3
  308. package/dist/tools/act-helpers.js +4 -4
  309. package/dist/tools/act-tools.d.ts +0 -8
  310. package/dist/tools/act-tools.js +84 -115
  311. package/dist/tools/act-view.js +7 -7
  312. package/dist/tools/alias-args.d.ts +17 -0
  313. package/dist/tools/alias-args.js +25 -0
  314. package/dist/tools/assert-grade.js +9 -9
  315. package/dist/tools/assert-verdict.d.ts +3 -1
  316. package/dist/tools/assert-verdict.js +8 -5
  317. package/dist/tools/browser-tools.js +30 -11
  318. package/dist/tools/contract-tools.js +1 -1
  319. package/dist/tools/coverage-identity.d.ts +34 -0
  320. package/dist/tools/coverage-identity.js +46 -0
  321. package/dist/tools/coverage-tools.js +10 -14
  322. package/dist/tools/dynamic-tools.d.ts +2 -1
  323. package/dist/tools/dynamic-tools.js +90 -18
  324. package/dist/tools/error-recovery.d.ts +20 -2
  325. package/dist/tools/error-recovery.js +179 -4
  326. package/dist/tools/feedback-tools.d.ts +12 -1
  327. package/dist/tools/feedback-tools.js +28 -5
  328. package/dist/tools/invoke-tool.d.ts +0 -6
  329. package/dist/tools/invoke-tool.js +57 -32
  330. package/dist/tools/lease-hint.d.ts +17 -0
  331. package/dist/tools/lease-hint.js +24 -0
  332. package/dist/tools/lease-tools.js +6 -5
  333. package/dist/tools/merge-tools.d.ts +10 -1
  334. package/dist/tools/merge-tools.js +9 -3
  335. package/dist/tools/merged-name-redirect.d.ts +13 -0
  336. package/dist/tools/merged-name-redirect.js +55 -0
  337. package/dist/tools/navigate-result.d.ts +17 -0
  338. package/dist/tools/navigate-result.js +31 -0
  339. package/dist/tools/observe-tools.js +54 -20
  340. package/dist/tools/observed-nothing.d.ts +25 -0
  341. package/dist/tools/observed-nothing.js +33 -0
  342. package/dist/tools/query-paginate.js +2 -2
  343. package/dist/tools/query-shape.d.ts +18 -0
  344. package/dist/tools/query-shape.js +39 -0
  345. package/dist/tools/read-tools.js +81 -20
  346. package/dist/tools/real-input-attempt.d.ts +28 -0
  347. package/dist/tools/real-input-attempt.js +88 -0
  348. package/dist/tools/reconcile-tools.js +7 -7
  349. package/dist/tools/reload-result.d.ts +14 -0
  350. package/dist/tools/reload-result.js +29 -0
  351. package/dist/tools/snapshot-delta.d.ts +4 -5
  352. package/dist/tools/snapshot-delta.js +0 -0
  353. package/dist/tools/tool-kit.d.ts +8 -0
  354. package/dist/tools/tool-surface.d.ts +83 -0
  355. package/dist/tools/tool-surface.js +204 -0
  356. package/dist/tools/tools-helpers.js +6 -6
  357. package/dist/tools/tools.d.ts +15 -0
  358. package/dist/tools/tools.js +53 -17
  359. package/dist/tools/unadvertised-help.d.ts +5 -0
  360. package/dist/tools/unadvertised-help.js +39 -0
  361. package/dist/trace.d.ts +69 -0
  362. package/dist/trace.js +150 -0
  363. package/dist/update/nudge-credit.d.ts +4 -0
  364. package/dist/update/nudge-credit.js +53 -0
  365. package/dist/update/sdk-sync.d.ts +21 -0
  366. package/dist/update/sdk-sync.js +42 -0
  367. package/dist/update/update-checker.js +3 -3
  368. package/dist/update/update-nudge.d.ts +63 -6
  369. package/dist/update/update-nudge.js +128 -15
  370. package/dist/update/updater.d.ts +1 -9
  371. package/dist/update/updater.js +13 -8
  372. package/dist/version/peer-announce.d.ts +4 -0
  373. package/dist/version/peer-announce.js +30 -0
  374. package/dist/{server-version.js → version/server-version.js} +5 -1
  375. package/dist/version/version-nudge.d.ts +28 -0
  376. package/dist/version/version-nudge.js +40 -0
  377. package/dist/version/version-skew.d.ts +66 -0
  378. package/dist/version/version-skew.js +71 -0
  379. package/dist/visual/visual-diff.js +2 -2
  380. package/dist/visual/visual-tools.js +4 -4
  381. package/docs/agent-cheatsheet.md +85 -0
  382. package/docs/architecture.md +113 -0
  383. package/docs/benchmarks.md +255 -0
  384. package/docs/debugging.md +102 -0
  385. package/docs/deploy-checks.md +69 -0
  386. package/docs/desktop-apps.md +256 -0
  387. package/docs/enterprise.md +62 -0
  388. package/docs/fixtures.md +28 -0
  389. package/docs/flows.md +166 -0
  390. package/docs/getting-started.md +469 -0
  391. package/docs/human-control.md +63 -0
  392. package/docs/integration-patterns.md +171 -0
  393. package/docs/local-registry.md +126 -0
  394. package/docs/mint.json +61 -0
  395. package/docs/multi-agent-testing.md +55 -0
  396. package/docs/platform-integration.md +122 -0
  397. package/docs/telemetry-contract.md +214 -0
  398. package/docs/telemetry.md +184 -0
  399. package/docs/testing.md +82 -0
  400. package/docs/token-efficiency.md +80 -0
  401. package/docs/usage.md +1046 -0
  402. package/package.json +8 -5
  403. package/dist/cli-launch.js +0 -112
  404. package/dist/daemon-resilience.d.ts +0 -22
  405. package/dist/daemon-resilience.js +0 -85
  406. package/dist/mcp-proxy.d.ts +0 -67
  407. package/dist/mcp-proxy.js +0 -337
  408. package/dist/tools/profiles.d.ts +0 -37
  409. package/dist/tools/profiles.js +0 -139
  410. /package/dist/{pairing-token.d.ts → bridge/pairing-token.d.ts} +0 -0
  411. /package/dist/{token-auth.d.ts → bridge/token-auth.d.ts} +0 -0
  412. /package/dist/{token-auth.js → bridge/token-auth.js} +0 -0
  413. /package/dist/{cli-doctor.d.ts → cli/cli-doctor.d.ts} +0 -0
  414. /package/dist/{cli-verify.d.ts → cli/cli-verify.d.ts} +0 -0
  415. /package/dist/{cloud-cli.d.ts → cli/cloud-cli.d.ts} +0 -0
  416. /package/dist/{server-version.d.ts → version/server-version.d.ts} +0 -0
@@ -1,19 +1,61 @@
1
1
  import { join } from 'node:path';
2
2
  import { homedir } from 'node:os';
3
- import { mkdirSync, readFileSync, writeFileSync, existsSync, unlinkSync, openSync, closeSync, readdirSync, } from 'node:fs';
3
+ import { mkdirSync, readFileSync, writeFileSync, existsSync, unlinkSync, openSync, closeSync, readdirSync, statSync, renameSync, } from 'node:fs';
4
4
  import { spawn } from 'node:child_process';
5
- import { daemonRegistryFileName, daemonRegistryPort, DaemonRegistryEntrySchema, pickDaemonPort, } from '@reticlehq/core';
6
- const RETICLE_HOME = join(homedir(), '.reticle');
7
- function pidPath(port, home = RETICLE_HOME) {
5
+ import { daemonRegistryFileName, ReticleEnv } from '@reticlehq/core';
6
+ import { log } from '../log.js';
7
+ /** Env override for the whole state directory — see ReticleEnv.STATE_DIR. */
8
+ export const STATE_DIR_ENV = ReticleEnv.STATE_DIR;
9
+ /**
10
+ * Where the daemon keeps pidfiles, the discovery registry and its logs.
11
+ *
12
+ * `~/.reticle` unless overridden. The override exists because a read-only $HOME is not hypothetical
13
+ * — a sandboxed agent or a locked-down Windows profile cannot write there, and until now that meant
14
+ * the daemon could not start at all. Read per call rather than captured at import, so a test (and a
15
+ * wrapper that sets it late) sees the current value.
16
+ */
17
+ export function reticleStateHome() {
18
+ const override = process.env[STATE_DIR_ENV];
19
+ if (override !== undefined && override.length > 0)
20
+ return override;
21
+ return join(homedir(), '.reticle');
22
+ }
23
+ function pidPath(port, home = reticleStateHome()) {
8
24
  return join(home, `daemon-${port}.pid`);
9
25
  }
10
26
  function registryPath(port) {
11
- return join(RETICLE_HOME, daemonRegistryFileName(port));
27
+ return join(reticleStateHome(), daemonRegistryFileName(port));
12
28
  }
13
29
  export function logPath(port) {
14
- return join(RETICLE_HOME, `daemon-${port}.log`);
30
+ return join(reticleStateHome(), `daemon-${port}.log`);
15
31
  }
16
- export function readPid(port, home = RETICLE_HOME) {
32
+ /**
33
+ * How large a daemon log may get before it is rolled over to `<name>.1`.
34
+ *
35
+ * It was unbounded. A real dev machine reached **24MB** on one port, which is not merely untidy: it
36
+ * is the difference between a log somebody opens and a log somebody gives up on, and it is disk that
37
+ * nothing ever reclaims. One previous generation is kept, because the question people bring to this
38
+ * file ("what happened just now?") is answered by the current one and the question they bring next
39
+ * ("and just before that?") is answered by the other.
40
+ */
41
+ export const MAX_DAEMON_LOG_BYTES = 8 * 1024 * 1024;
42
+ /**
43
+ * Roll the log over if it has grown past the cap. Best-effort: a daemon must still start when the
44
+ * log cannot be rotated, because failing to launch over housekeeping is strictly worse than a large
45
+ * file.
46
+ */
47
+ export function rotateDaemonLog(path, deps, max = MAX_DAEMON_LOG_BYTES) {
48
+ try {
49
+ if (max >= deps.fileSize(path))
50
+ return false;
51
+ deps.renameFile(path, `${path}.1`);
52
+ return true;
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }
58
+ export function readPid(port, home = reticleStateHome()) {
17
59
  const path = pidPath(port, home);
18
60
  if (!existsSync(path))
19
61
  return null;
@@ -30,7 +72,7 @@ export function isAlive(pid) {
30
72
  }
31
73
  }
32
74
  export function writePid(port) {
33
- mkdirSync(RETICLE_HOME, { recursive: true });
75
+ mkdirSync(reticleStateHome(), { recursive: true });
34
76
  writeFileSync(pidPath(port), String(process.pid), 'utf8');
35
77
  }
36
78
  /**
@@ -39,7 +81,7 @@ export function writePid(port) {
39
81
  * the orphan-race guard: a losing childB (EADDRINUSE) must not delete the winning childA's live pidfile.
40
82
  */
41
83
  export function shouldRemovePid(owner, expectedPid, alive) {
42
- return owner === null || owner === expectedPid || !alive;
84
+ return null === owner || owner === expectedPid || !alive;
43
85
  }
44
86
  export function removePid(port, expectedPid = process.pid) {
45
87
  const path = pidPath(port);
@@ -66,14 +108,14 @@ export function writeDaemonRegistry(port, meta) {
66
108
  ...(meta.projectId !== undefined ? { projectId: meta.projectId } : {}),
67
109
  };
68
110
  try {
69
- mkdirSync(RETICLE_HOME, { recursive: true });
111
+ mkdirSync(reticleStateHome(), { recursive: true });
70
112
  writeFileSync(registryPath(port), JSON.stringify(entry), 'utf8');
71
113
  }
72
114
  catch {
73
115
  // discovery is a convenience — never block startup on it
74
116
  }
75
117
  }
76
- export function removeDaemonRegistry(port) {
118
+ function removeDaemonRegistry(port) {
77
119
  const path = registryPath(port);
78
120
  try {
79
121
  if (existsSync(path))
@@ -83,34 +125,6 @@ export function removeDaemonRegistry(port) {
83
125
  // racing another cleaner — already gone
84
126
  }
85
127
  }
86
- /**
87
- * Discover the port of a live daemon serving `projectId` by reading the registry entries in ~/.reticle.
88
- * Returns null when none matches (caller falls back to the default port — never guesses a mismatched
89
- * daemon). Stale entries (crashed daemons) are ignored via the pid liveness probe.
90
- */
91
- export function discoverDaemonPortForProject(projectId) {
92
- const entries = [];
93
- let files;
94
- try {
95
- files = readdirSync(RETICLE_HOME);
96
- }
97
- catch {
98
- return null; // no ~/.reticle yet
99
- }
100
- for (const file of files) {
101
- if (daemonRegistryPort(file) === null)
102
- continue;
103
- try {
104
- const parsed = DaemonRegistryEntrySchema.safeParse(JSON.parse(readFileSync(join(RETICLE_HOME, file), 'utf8')));
105
- if (parsed.success)
106
- entries.push(parsed.data);
107
- }
108
- catch {
109
- // unreadable/corrupt entry — skip it
110
- }
111
- }
112
- return pickDaemonPort(entries, projectId, isAlive);
113
- }
114
128
  export function isRunning(port) {
115
129
  const pid = readPid(port);
116
130
  return pid !== null && isAlive(pid);
@@ -124,12 +138,12 @@ export function discoverDaemonPort() {
124
138
  reclaimStaleDaemons(); // sweep crashed daemons' stale pidfiles before scanning for live ones
125
139
  let found = null;
126
140
  try {
127
- for (const file of readdirSync(RETICLE_HOME)) {
141
+ for (const file of readdirSync(reticleStateHome())) {
128
142
  const m = /^daemon-(\d+)\.pid$/.exec(file);
129
- if (m === null)
143
+ if (null === m)
130
144
  continue;
131
145
  const port = Number(m[1]);
132
- if (isRunning(port) && (found === null || port < found))
146
+ if (isRunning(port) && (null === found || port < found))
133
147
  found = port;
134
148
  }
135
149
  }
@@ -144,7 +158,7 @@ export function discoverDaemonPort() {
144
158
  * Returns the ports reclaimed. `home` and `pidAlive` are injectable for testing (default to the real
145
159
  * ~/.reticle and the process.kill(pid,0) liveness probe).
146
160
  */
147
- export function reclaimStaleDaemons(home = RETICLE_HOME, pidAlive = isAlive) {
161
+ export function reclaimStaleDaemons(home = reticleStateHome(), pidAlive = isAlive) {
148
162
  const reclaimed = [];
149
163
  let files;
150
164
  try {
@@ -155,7 +169,7 @@ export function reclaimStaleDaemons(home = RETICLE_HOME, pidAlive = isAlive) {
155
169
  }
156
170
  for (const file of files) {
157
171
  const match = /^daemon-(\d+)\.pid$/.exec(file);
158
- if (match === null)
172
+ if (null === match)
159
173
  continue;
160
174
  const path = join(home, file);
161
175
  let pid = null;
@@ -167,7 +181,7 @@ export function reclaimStaleDaemons(home = RETICLE_HOME, pidAlive = isAlive) {
167
181
  catch {
168
182
  pid = null; // unreadable pidfile counts as stale
169
183
  }
170
- if (pid === null || !pidAlive(pid)) {
184
+ if (null === pid || !pidAlive(pid)) {
171
185
  try {
172
186
  unlinkSync(path);
173
187
  removeDaemonRegistry(Number(match[1])); // drop the sidecar discovery entry too
@@ -180,11 +194,13 @@ export function reclaimStaleDaemons(home = RETICLE_HOME, pidAlive = isAlive) {
180
194
  }
181
195
  return reclaimed;
182
196
  }
183
- export function defaultSpawnDaemonDeps() {
197
+ function defaultSpawnDaemonDeps() {
184
198
  return {
185
- home: RETICLE_HOME,
199
+ home: reticleStateHome(),
186
200
  openFile: openSync,
187
201
  closeFile: closeSync,
202
+ fileSize: (path) => (existsSync(path) ? statSync(path).size : 0),
203
+ renameFile: renameSync,
188
204
  spawnChild: (command, args, options) => spawn(command, [...args], { detached: options.detached, stdio: [...options.stdio] }),
189
205
  pidAlive: isAlive,
190
206
  };
@@ -198,7 +214,20 @@ export function defaultSpawnDaemonDeps() {
198
214
  * pattern as reclaimStaleDaemons(home, pidAlive).
199
215
  */
200
216
  export function spawnDaemon(nodeExec, scriptPath, args, port, deps = defaultSpawnDaemonDeps()) {
201
- mkdirSync(deps.home, { recursive: true });
217
+ try {
218
+ mkdirSync(deps.home, { recursive: true });
219
+ }
220
+ catch (err) {
221
+ // The log-file open below has always been guarded; this was not, so the ONE failure a sandboxed
222
+ // agent actually hits — a read-only $HOME — escaped as a raw EACCES naming nothing. Refuse
223
+ // loudly, and name the way out.
224
+ log('reticle_daemon_state_unwritable', {
225
+ home: deps.home,
226
+ error: err instanceof Error ? err.message : String(err),
227
+ fix: `Reticle could not create its state directory. Set ${STATE_DIR_ENV} to a writable path and retry.`,
228
+ });
229
+ return false;
230
+ }
202
231
  const pidFilePath = join(deps.home, `daemon-${port}.pid`);
203
232
  const logFilePath = join(deps.home, `daemon-${port}.log`);
204
233
  // O_EXCL spawn-lock: only the FIRST racer to create the pidfile spawns. A concurrent second gets
@@ -220,6 +249,9 @@ export function spawnDaemon(nodeExec, scriptPath, args, port, deps = defaultSpaw
220
249
  return false; // lost a concurrent reclaim race
221
250
  }
222
251
  }
252
+ // Roll the log BEFORE opening the append handle, or the daemon writes into the renamed file
253
+ // through an fd that no longer matches any name anyone can find.
254
+ rotateDaemonLog(logFilePath, deps);
223
255
  let logFd;
224
256
  try {
225
257
  logFd = deps.openFile(logFilePath, 'a');
@@ -0,0 +1,32 @@
1
+ /**
2
+ * How long an idle daemon is given before it shuts itself down.
3
+ *
4
+ * It used to be one number for every case, and at 5 minutes that number killed live runs: 187
5
+ * `reticle_daemon_idle_shutdown` lines on one user's machine, with their editor's Reticle
6
+ * disconnecting each time; and in the fixtures gate it fired during long dependency installs, so apps
7
+ * that booted afterwards hit ERR_CONNECTION_REFUSED and were scored as install failures when nothing
8
+ * was wrong with their install.
9
+ *
10
+ * Reverting is not an option. Daemons used to sit idle a median of 28 minutes at a 0.04% duty cycle
11
+ * because "an agent is attached" alone kept one alive for a whole editor session, and the widened
12
+ * rule is what fixed that.
13
+ *
14
+ * The distinction is TIME, not state. Mid-install an attached daemon is state-identical to one in an
15
+ * empty directory — nothing has been asked of either. What differs is how long it is reasonable to
16
+ * wait: quiet with a client attached means the human is thinking or the install is slow; the same
17
+ * quiet with nobody attached means the daemon is unwanted. Both still exit.
18
+ */
19
+ /**
20
+ * An attached daemon waits this many times the base grace. Six is chosen against the two measurements
21
+ * that matter: the default base is 5 minutes, so an attached daemon survives 30 — comfortably past
22
+ * the slow installs that were killing runs, and comfortably short of the 28-minute median idle the
23
+ * shorter rule was introduced to end.
24
+ */
25
+ export declare const ATTACHED_GRACE_MULTIPLIER = 6;
26
+ /**
27
+ * @param overrideMs an explicit attached grace (RETICLE_IDLE_ATTACHED_MS). Derived from the base when
28
+ * absent — but the derived value is arithmetic on a number the caller also controls, and a test that
29
+ * has to reason about both is a test that breaks for the wrong reason. Configuring it directly is
30
+ * what lets the e2e spec assert the BEHAVIOUR (an attached daemon exits) without racing a product.
31
+ */
32
+ export declare function idleGraceMs(baseMs: number, agentAttached: boolean, overrideMs?: number): number;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * How long an idle daemon is given before it shuts itself down.
3
+ *
4
+ * It used to be one number for every case, and at 5 minutes that number killed live runs: 187
5
+ * `reticle_daemon_idle_shutdown` lines on one user's machine, with their editor's Reticle
6
+ * disconnecting each time; and in the fixtures gate it fired during long dependency installs, so apps
7
+ * that booted afterwards hit ERR_CONNECTION_REFUSED and were scored as install failures when nothing
8
+ * was wrong with their install.
9
+ *
10
+ * Reverting is not an option. Daemons used to sit idle a median of 28 minutes at a 0.04% duty cycle
11
+ * because "an agent is attached" alone kept one alive for a whole editor session, and the widened
12
+ * rule is what fixed that.
13
+ *
14
+ * The distinction is TIME, not state. Mid-install an attached daemon is state-identical to one in an
15
+ * empty directory — nothing has been asked of either. What differs is how long it is reasonable to
16
+ * wait: quiet with a client attached means the human is thinking or the install is slow; the same
17
+ * quiet with nobody attached means the daemon is unwanted. Both still exit.
18
+ */
19
+ /**
20
+ * An attached daemon waits this many times the base grace. Six is chosen against the two measurements
21
+ * that matter: the default base is 5 minutes, so an attached daemon survives 30 — comfortably past
22
+ * the slow installs that were killing runs, and comfortably short of the 28-minute median idle the
23
+ * shorter rule was introduced to end.
24
+ */
25
+ export const ATTACHED_GRACE_MULTIPLIER = 6;
26
+ /**
27
+ * @param overrideMs an explicit attached grace (RETICLE_IDLE_ATTACHED_MS). Derived from the base when
28
+ * absent — but the derived value is arithmetic on a number the caller also controls, and a test that
29
+ * has to reason about both is a test that breaks for the wrong reason. Configuring it directly is
30
+ * what lets the e2e spec assert the BEHAVIOUR (an attached daemon exits) without racing a product.
31
+ */
32
+ export function idleGraceMs(baseMs, agentAttached, overrideMs) {
33
+ // A non-positive base means the watcher is disabled; multiplying must not resurrect it.
34
+ if (baseMs <= 0)
35
+ return baseMs;
36
+ if (!agentAttached)
37
+ return baseMs;
38
+ return overrideMs !== undefined && overrideMs > 0
39
+ ? overrideMs
40
+ : baseMs * ATTACHED_GRACE_MULTIPLIER;
41
+ }
@@ -5,6 +5,14 @@ export interface IdleShutdownOptions {
5
5
  onShutdown: () => void;
6
6
  /** Continuous-idle window before shutdown. `0` (or negative) disables the watcher entirely. */
7
7
  graceMs: number;
8
+ /**
9
+ * Is an agent's MCP client attached right now? An attached daemon gets a much longer grace — quiet
10
+ * with a client present means a slow install or a thinking human, not an unwanted daemon. It still
11
+ * exits. See idle-grace.
12
+ */
13
+ agentAttached?: () => boolean;
14
+ /** Explicit attached grace (RETICLE_IDLE_ATTACHED_MS); derived from graceMs when absent. */
15
+ attachedGraceMs?: number;
8
16
  /** How often to re-check idleness. */
9
17
  checkIntervalMs?: number;
10
18
  /** Injected clock — never read wall-clock time in logic (repo rule). */
@@ -30,4 +38,9 @@ export declare class IdleShutdown {
30
38
  * Resolve the idle-shutdown grace from `RETICLE_IDLE_SHUTDOWN_MS`: a non-negative integer of milliseconds,
31
39
  * `0` to disable. Anything missing/invalid falls back to the default. Pure.
32
40
  */
41
+ /**
42
+ * The idle re-check cadence, from the environment. Same shape as the grace resolver: a bad value
43
+ * falls back rather than throwing, and 0/negative is not a legal cadence (it would spin).
44
+ */
45
+ export declare function resolveIdleCheckMs(raw: string | undefined): number;
33
46
  export declare function resolveIdleShutdownMs(raw: string | undefined): number;
@@ -1,5 +1,6 @@
1
1
  import { SESSION_LIFECYCLE } from '@reticlehq/core';
2
- import { log } from './log.js';
2
+ import { idleGraceMs } from './idle-grace.js';
3
+ import { log } from '../log.js';
3
4
  /**
4
5
  * Self-terminates an idle daemon so Reticle never lingers eating a user's resources (the daemon process,
5
6
  * any headless Chromium the pool launched, and the bound port) after the editor closes. The `reticle mcp`
@@ -15,12 +16,16 @@ export class IdleShutdown {
15
16
  #isIdle;
16
17
  #onShutdown;
17
18
  #graceMs;
19
+ #agentAttached;
20
+ #attachedGraceMs;
18
21
  #checkMs;
19
22
  #clock;
20
23
  constructor(opts) {
21
24
  this.#isIdle = opts.isIdle;
22
25
  this.#onShutdown = opts.onShutdown;
23
26
  this.#graceMs = opts.graceMs;
27
+ this.#agentAttached = opts.agentAttached;
28
+ this.#attachedGraceMs = opts.attachedGraceMs;
24
29
  this.#checkMs = opts.checkIntervalMs ?? SESSION_LIFECYCLE.DAEMON_IDLE_CHECK_MS;
25
30
  this.#clock = opts.clock ?? (() => Date.now());
26
31
  }
@@ -34,9 +39,15 @@ export class IdleShutdown {
34
39
  }
35
40
  const now = this.#clock();
36
41
  this.#idleSince ??= now;
37
- if (now - this.#idleSince >= this.#graceMs) {
42
+ // Read per CHECK, not at construction: a client attaches and detaches during a daemon's life, and
43
+ // the question "how long should this quiet be tolerated" is only answerable now. See idle-grace.
44
+ const grace = idleGraceMs(this.#graceMs, this.#agentAttached?.() ?? false, this.#attachedGraceMs);
45
+ if (now - this.#idleSince >= grace) {
38
46
  this.#fired = true;
39
- log('reticle_daemon_idle_shutdown', { idleMs: now - this.#idleSince });
47
+ log('reticle_daemon_idle_shutdown', {
48
+ idleMs: now - this.#idleSince,
49
+ attached: this.#agentAttached?.() ?? false,
50
+ });
40
51
  this.#onShutdown();
41
52
  }
42
53
  }
@@ -57,8 +68,20 @@ export class IdleShutdown {
57
68
  * Resolve the idle-shutdown grace from `RETICLE_IDLE_SHUTDOWN_MS`: a non-negative integer of milliseconds,
58
69
  * `0` to disable. Anything missing/invalid falls back to the default. Pure.
59
70
  */
71
+ /**
72
+ * The idle re-check cadence, from the environment. Same shape as the grace resolver: a bad value
73
+ * falls back rather than throwing, and 0/negative is not a legal cadence (it would spin).
74
+ */
75
+ export function resolveIdleCheckMs(raw) {
76
+ if (raw === undefined || '' === raw.trim())
77
+ return SESSION_LIFECYCLE.DAEMON_IDLE_CHECK_MS;
78
+ const n = Number(raw);
79
+ if (!Number.isFinite(n) || n <= 0)
80
+ return SESSION_LIFECYCLE.DAEMON_IDLE_CHECK_MS;
81
+ return Math.floor(n);
82
+ }
60
83
  export function resolveIdleShutdownMs(raw) {
61
- if (raw === undefined || raw.trim() === '')
84
+ if (raw === undefined || '' === raw.trim())
62
85
  return SESSION_LIFECYCLE.DAEMON_IDLE_SHUTDOWN_MS;
63
86
  const n = Number(raw);
64
87
  if (!Number.isFinite(n) || n < 0)
@@ -107,11 +107,11 @@ export function buildDomainModel(flows, contract, runs = []) {
107
107
  };
108
108
  }
109
109
  function buildSummary(flowCount, coverage, gaps, topRisk) {
110
- if (flowCount === 0) {
110
+ if (0 === flowCount) {
111
111
  return 'No saved flows yet — record the critical journeys (reticle_record{action:"start"}) so the agent learns the app.';
112
112
  }
113
113
  const parts = [
114
- `${String(flowCount)} flow${flowCount === 1 ? '' : 's'}: ${String(coverage.asserted)} asserted, ${String(coverage.presenceOnly)} presence-only, ${String(coverage.assertionFree)} assertion-free`,
114
+ `${String(flowCount)} flow${1 === flowCount ? '' : 's'}: ${String(coverage.asserted)} asserted, ${String(coverage.presenceOnly)} presence-only, ${String(coverage.assertionFree)} assertion-free`,
115
115
  ];
116
116
  if (topRisk !== undefined) {
117
117
  parts.push(`test first: ${topRisk.name} (${topRisk.reason})`);
@@ -25,19 +25,19 @@ const ERROR_KEYS = ['error', 'errors', 'errorMessage', 'failureReason'];
25
25
  /** Arrays worth walking for per-item outcomes. Bulk APIs converge on a handful of names. */
26
26
  const ITEM_KEYS = ['results', 'items', 'data', 'records', 'responses'];
27
27
  function isRecord(value) {
28
- return typeof value === 'object' && value !== null && !Array.isArray(value);
28
+ return 'object' === typeof value && value !== null && !Array.isArray(value);
29
29
  }
30
30
  /** Whether one object reports a failure of its own. */
31
31
  function reportsFailure(item) {
32
32
  if (!isRecord(item))
33
33
  return false;
34
34
  for (const flag of FAILURE_FLAGS) {
35
- if (item[flag] === false)
35
+ if (false === item[flag])
36
36
  return true;
37
37
  }
38
38
  for (const key of ERROR_KEYS) {
39
39
  const value = item[key];
40
- if (value === undefined || value === null)
40
+ if (value === undefined || null === value)
41
41
  continue;
42
42
  if (Array.isArray(value)) {
43
43
  if (value.length > 0)
@@ -45,7 +45,7 @@ function reportsFailure(item) {
45
45
  continue;
46
46
  }
47
47
  // An empty string or an explicit null is an API saying "no error", not an error.
48
- if (typeof value === 'string' && value.length === 0)
48
+ if ('string' === typeof value && 0 === value.length)
49
49
  continue;
50
50
  return true;
51
51
  }
@@ -55,7 +55,7 @@ function reportsFailure(item) {
55
55
  function itemOutcomes(body) {
56
56
  for (const key of ITEM_KEYS) {
57
57
  const list = body[key];
58
- if (!Array.isArray(list) || list.length === 0)
58
+ if (!Array.isArray(list) || 0 === list.length)
59
59
  continue;
60
60
  const failed = list.filter(reportsFailure).length;
61
61
  return { failed, total: list.length };
@@ -63,7 +63,7 @@ function itemOutcomes(body) {
63
63
  return { failed: 0, total: 0 };
64
64
  }
65
65
  function parse(raw) {
66
- if (typeof raw !== 'string' || raw.length === 0)
66
+ if (typeof raw !== 'string' || 0 === raw.length)
67
67
  return undefined;
68
68
  try {
69
69
  const parsed = JSON.parse(raw);
@@ -87,8 +87,8 @@ export function findBodyFailures(events) {
87
87
  const body = parse(event.data['responseBody']);
88
88
  if (body === undefined)
89
89
  continue;
90
- const url = typeof event.data['url'] === 'string' ? event.data['url'] : '';
91
- const method = typeof event.data['method'] === 'string' ? event.data['method'] : '';
90
+ const url = 'string' === typeof event.data['url'] ? event.data['url'] : '';
91
+ const method = 'string' === typeof event.data['method'] ? event.data['method'] : '';
92
92
  const where = `${method} ${url} → ${String(status)}`;
93
93
  const { failed, total } = itemOutcomes(body);
94
94
  if (failed > 0) {
@@ -50,4 +50,4 @@ export interface ContradictionOptions {
50
50
  */
51
51
  mutatedWithin?: number | undefined;
52
52
  }
53
- export declare function findContradictions(events: readonly ReticleEvent[], options?: ContradictionOptions): Contradiction[];
53
+ export declare function findContradictions(allEvents: readonly ReticleEvent[], options?: ContradictionOptions): Contradiction[];
@@ -1,4 +1,4 @@
1
- import { ContradictionKind, EventType, MUTATING_METHODS } from '@reticlehq/core';
1
+ import { ContradictionKind, EventType, MUTATING_METHODS, isDevToolingUrl, } from '@reticlehq/core';
2
2
  import { findStaleResponses } from './stale-response.js';
3
3
  import { findBodyFailures } from './body-failures.js';
4
4
  import { findEchoMismatches } from './echo-mismatch.js';
@@ -14,7 +14,7 @@ function netCall(e) {
14
14
  // Neither present = no verdict was ever reported, which stays undefined all the way through.
15
15
  ok: e.data['ok'] === undefined && status === undefined
16
16
  ? undefined
17
- : e.data['ok'] === true || (e.data['ok'] === undefined && (status ?? 0) < 400),
17
+ : true === e.data['ok'] || (e.data['ok'] === undefined && (status ?? 0) < 400),
18
18
  };
19
19
  }
20
20
  /**
@@ -95,7 +95,7 @@ function failureAcknowledged(events) {
95
95
  return false;
96
96
  const path = asString(e.data['path']) ?? '';
97
97
  const value = e.data['value'];
98
- return ACKNOWLEDGED.test(path) || (typeof value === 'string' && ACKNOWLEDGED.test(value));
98
+ return ACKNOWLEDGED.test(path) || ('string' === typeof value && ACKNOWLEDGED.test(value));
99
99
  });
100
100
  }
101
101
  function describe(call) {
@@ -125,13 +125,44 @@ const MUST_DO_SOMETHING = new Set(['click', 'dblclick', 'submit']);
125
125
  */
126
126
  function didNothing(events, requests, mutatedWithin) {
127
127
  if (mutatedWithin === undefined)
128
- return events.length === 0;
128
+ return 0 === events.length;
129
129
  if (mutatedWithin > 0 || requests.length > 0)
130
130
  return false;
131
131
  return !events.some((e) => e.type === EventType.ROUTE_CHANGE || e.type === EventType.VISIBLE_SHOWN);
132
132
  }
133
- export function findContradictions(events, options = {}) {
133
+ /** Net-shaped events — the only ones that carry a URL a dev-tooling channel could occupy. */
134
+ const NET_TYPES = new Set([
135
+ EventType.NET_PENDING,
136
+ EventType.NET_REQUEST,
137
+ EventType.NET_STREAM,
138
+ ]);
139
+ /**
140
+ * Split the window into the app's traffic and the dev toolchain's own (see `DevToolingChannel`).
141
+ *
142
+ * NOTHING below may judge the toolchain. Reported from a real drive: a correct Next.js navigation
143
+ * graded `verified: "no"` because the dev overlay was resolving a source map for an unrelated React
144
+ * key warning, and that in-flight `POST /__nextjs_original-stack-frames` read as "the UI advanced
145
+ * over a request that never settled". Every app that logs one dev warning got a false negative on
146
+ * every action. The overlay's own 404s and duplicate fetches are the same story on other checks,
147
+ * which is why the split happens ONCE here rather than in the one check that reported it.
148
+ */
149
+ function splitDevTooling(events) {
150
+ const ignored = [];
151
+ const app = events.filter((e) => {
152
+ if (!NET_TYPES.has(e.type))
153
+ return true;
154
+ const url = asString(e.data['url']);
155
+ if (!isDevToolingUrl(url))
156
+ return true;
157
+ if (url !== undefined && !ignored.includes(url))
158
+ ignored.push(url);
159
+ return false;
160
+ });
161
+ return { app, ignored };
162
+ }
163
+ export function findContradictions(allEvents, options = {}) {
134
164
  const found = [];
165
+ const { app: events, ignored: ignoredDevTooling } = splitDevTooling(allEvents);
135
166
  const settled = events.filter((e) => e.type === EventType.NET_REQUEST).map(netCall);
136
167
  // ── Overlapping reads that settled out of order ─────────────────────────────────────────────
137
168
  // Independent of any action, so it runs on every window: the race is a property of the timeline.
@@ -160,7 +191,7 @@ export function findContradictions(events, options = {}) {
160
191
  },
161
192
  ];
162
193
  }
163
- const failed = settled.filter((c) => c.ok === false);
194
+ const failed = settled.filter((c) => false === c.ok);
164
195
  const advanced = uiAdvanced(events);
165
196
  const signals = events
166
197
  .filter((e) => e.type === EventType.SIGNAL)
@@ -170,7 +201,19 @@ export function findContradictions(events, options = {}) {
170
201
  // the control worked, the DESTINATION is empty — which is why every "did the click do something"
171
202
  // heuristic passes it, a route change being unambiguously something.
172
203
  const routed = events.some((e) => e.type === EventType.ROUTE_CHANGE);
173
- const rendered = events.some((e) => e.type === EventType.DOM_ADDED || e.type === EventType.DOM_REMOVED);
204
+ // `dom.text` counts as rendered, and it has to: React reconciles a destination IN PLACE far more
205
+ // often than it adds nodes. Measured on three ordinary sidebar navigations of the bench app — every
206
+ // one emitted { dom.attr:2, dom.text:2, render.commit, state.change } and ZERO dom.added/removed,
207
+ // so all three were flagged as blank destinations, `verified` came back "no" on a correct green,
208
+ // and a bug_found was emitted for a navigation that worked.
209
+ //
210
+ // Deliberately NOT `dom.attr`: the nav link marks itself active whether or not the destination
211
+ // rendered, which is the true positive this rule exists for. Deliberately NOT `render.commit`
212
+ // either: React commits a render for a component that returns null, which is one of the very bugs
213
+ // named in `detail` below.
214
+ const rendered = events.some((e) => e.type === EventType.DOM_ADDED ||
215
+ e.type === EventType.DOM_REMOVED ||
216
+ e.type === EventType.DOM_TEXT);
174
217
  const fetched = events.some((e) => e.type === EventType.NET_REQUEST || e.type === EventType.NET_PENDING);
175
218
  if (routed && !rendered && !fetched) {
176
219
  found.push({
@@ -187,7 +230,7 @@ export function findContradictions(events, options = {}) {
187
230
  // Checked BEFORE the success-claim rules, because it is the sharper reading of the same events:
188
231
  // the app did not claim success, it claimed the wrong failure. Telling someone their password is
189
232
  // wrong while the backend is down sends them to fix something they cannot fix.
190
- const serverFaults = settled.filter((c) => c.ok === false && c.status !== undefined && c.status >= SERVER_FAULT_MIN);
233
+ const serverFaults = settled.filter((c) => false === c.ok && c.status !== undefined && c.status >= SERVER_FAULT_MIN);
191
234
  const userBlame = [
192
235
  ...signals.filter((name) => BLAMES_USER.test(name)),
193
236
  ...events
@@ -228,7 +271,7 @@ export function findContradictions(events, options = {}) {
228
271
  // Writes only: a GET that changes nothing is a prefetch; a POST that changes nothing is a lost
229
272
  // write, a response parsed into the void, or a render that never happened.
230
273
  if (!advanced) {
231
- const ignoredWrites = settled.filter((c) => c.ok === true && isMutating(c));
274
+ const ignoredWrites = settled.filter((c) => true === c.ok && isMutating(c));
232
275
  if (ignoredWrites.length > 0) {
233
276
  found.push({
234
277
  kind: ContradictionKind.RESPONSE_IGNORED,
@@ -274,7 +317,14 @@ export function findContradictions(events, options = {}) {
274
317
  kind: ContradictionKind.REQUEST_NEVER_SETTLED,
275
318
  claim: 'the UI moved forward and the action reported done',
276
319
  counter: `${String(inFlight.length)} request(s) were still in flight`,
277
- detail: inFlight.map((p) => describe(p.call)).join('; '),
320
+ // The exclusion is never silent: if the toolchain's own traffic was dropped from this count,
321
+ // the finding says which URLs, so an agent reading it can see what Reticle chose to ignore.
322
+ detail: [
323
+ inFlight.map((p) => describe(p.call)).join('; '),
324
+ ...(0 === ignoredDevTooling.length
325
+ ? []
326
+ : [`ignored as dev tooling: ${ignoredDevTooling.join(', ')}`]),
327
+ ].join(' — '),
278
328
  });
279
329
  }
280
330
  }