@reticlehq/server 2.4.0 → 2.6.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 (472) hide show
  1. package/SKILL.md +719 -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} +13 -1
  7. package/dist/{bridge.js → bridge/bridge.js} +95 -15
  8. package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +9 -11
  9. package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +18 -10
  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/cli-doctor.d.ts +1 -0
  20. package/dist/cli/cli-doctor.js +135 -0
  21. package/dist/{cli-flow-commands.d.ts → cli/cli-flow-commands.d.ts} +2 -2
  22. package/dist/{cli-flow-commands.js → cli/cli-flow-commands.js} +15 -15
  23. package/dist/{cli-launch.d.ts → cli/cli-launch.d.ts} +32 -3
  24. package/dist/cli/cli-launch.js +171 -0
  25. package/dist/cli/cli-parse-feedback.d.ts +31 -0
  26. package/dist/cli/cli-parse-feedback.js +56 -0
  27. package/dist/{cli-parse.d.ts → cli/cli-parse.d.ts} +6 -19
  28. package/dist/{cli-parse.js → cli/cli-parse.js} +118 -81
  29. package/dist/{cli-port.d.ts → cli/cli-port.d.ts} +12 -0
  30. package/dist/{cli-port.js → cli/cli-port.js} +9 -9
  31. package/dist/cli/cli-update-commands.d.ts +4 -0
  32. package/dist/cli/cli-update-commands.js +93 -0
  33. package/dist/{cli-verify.js → cli/cli-verify.js} +14 -14
  34. package/dist/{cloud-cli.js → cli/cloud-cli.js} +29 -24
  35. package/dist/cli/doctor-daemon-line.d.ts +37 -0
  36. package/dist/cli/doctor-daemon-line.js +35 -0
  37. package/dist/cli/port-holder.d.ts +43 -0
  38. package/dist/cli/port-holder.js +99 -0
  39. package/dist/cli.d.ts +2 -2
  40. package/dist/cli.js +302 -100
  41. package/dist/cloud/cloud-config.d.ts +0 -1
  42. package/dist/cloud/cloud-config.js +9 -9
  43. package/dist/cloud/cloud-sync.d.ts +1 -7
  44. package/dist/cloud/cloud-sync.js +12 -12
  45. package/dist/crawl/crawl-empty.d.ts +16 -0
  46. package/dist/crawl/crawl-empty.js +24 -0
  47. package/dist/crawl/crawl-tools.js +4 -1
  48. package/dist/crawl/crawl.js +26 -7
  49. package/dist/daemon/daemon-resilience.d.ts +73 -0
  50. package/dist/daemon/daemon-resilience.js +279 -0
  51. package/dist/daemon/daemon-usefulness.d.ts +61 -0
  52. package/dist/daemon/daemon-usefulness.js +62 -0
  53. package/dist/{daemon.d.ts → daemon/daemon.d.ts} +33 -8
  54. package/dist/{daemon.js → daemon/daemon.js} +105 -48
  55. package/dist/daemon/heartbeat.d.ts +115 -0
  56. package/dist/daemon/heartbeat.js +141 -0
  57. package/dist/daemon/idle-grace.d.ts +32 -0
  58. package/dist/daemon/idle-grace.js +41 -0
  59. package/dist/{idle-shutdown.d.ts → daemon/idle-shutdown.d.ts} +23 -2
  60. package/dist/{idle-shutdown.js → daemon/idle-shutdown.js} +37 -6
  61. package/dist/daemon/port-presence.d.ts +76 -0
  62. package/dist/daemon/port-presence.js +93 -0
  63. package/dist/daemon/state-dir.d.ts +7 -0
  64. package/dist/daemon/state-dir.js +59 -0
  65. package/dist/daemon/unclean-predecessor.d.ts +23 -0
  66. package/dist/daemon/unclean-predecessor.js +27 -0
  67. package/dist/domain/domain-model.js +2 -2
  68. package/dist/events/body-failures.js +8 -8
  69. package/dist/events/contradictions.d.ts +1 -1
  70. package/dist/events/contradictions.js +60 -10
  71. package/dist/events/echo-mismatch.js +10 -10
  72. package/dist/events/event-filters.js +6 -6
  73. package/dist/events/json-salvage.d.ts +2 -1
  74. package/dist/events/json-salvage.js +6 -6
  75. package/dist/events/observed-in-window.d.ts +12 -0
  76. package/dist/events/observed-in-window.js +21 -0
  77. package/dist/events/predicate-eval.d.ts +53 -20
  78. package/dist/events/predicate-eval.js +179 -55
  79. package/dist/events/predicate-parse.d.ts +23 -0
  80. package/dist/events/predicate-parse.js +54 -0
  81. package/dist/events/predicate.d.ts +7 -2
  82. package/dist/events/predicate.js +126 -35
  83. package/dist/events/reconcile.js +5 -5
  84. package/dist/events/ring-buffer.js +2 -17
  85. package/dist/events/stale-response.js +6 -6
  86. package/dist/events/testid-near-miss.d.ts +22 -0
  87. package/dist/events/testid-near-miss.js +33 -0
  88. package/dist/events/unit-mismatch.js +8 -8
  89. package/dist/flows/affected-tools.js +19 -4
  90. package/dist/flows/affected.js +1 -1
  91. package/dist/flows/annotate-target.d.ts +1 -0
  92. package/dist/flows/annotate-target.js +20 -0
  93. package/dist/flows/annotate-tools.js +25 -3
  94. package/dist/flows/annotate.js +17 -4
  95. package/dist/flows/assertion-integrity.d.ts +2 -1
  96. package/dist/flows/assertion-tiers-store.d.ts +3 -66
  97. package/dist/flows/assertion-tiers-store.js +2 -2
  98. package/dist/flows/attributed-failure.d.ts +17 -0
  99. package/dist/flows/attributed-failure.js +20 -0
  100. package/dist/flows/buddy-status.d.ts +2 -1
  101. package/dist/flows/buddy-status.js +2 -2
  102. package/dist/flows/coverage.d.ts +3 -2
  103. package/dist/flows/coverage.js +2 -2
  104. package/dist/flows/decision.js +24 -9
  105. package/dist/flows/empty-flow.d.ts +6 -0
  106. package/dist/flows/empty-flow.js +24 -0
  107. package/dist/flows/flake-store.d.ts +0 -1
  108. package/dist/flows/flake-store.js +1 -1
  109. package/dist/flows/flake.d.ts +2 -1
  110. package/dist/flows/flake.js +2 -2
  111. package/dist/flows/flow-classify.js +12 -0
  112. package/dist/flows/flow-replay-run.js +4 -4
  113. package/dist/flows/flow-replay.d.ts +6 -8
  114. package/dist/flows/flow-replay.js +132 -48
  115. package/dist/flows/flow-report.js +1 -1
  116. package/dist/flows/flow-scope.d.ts +1 -6
  117. package/dist/flows/flow-step-runners.d.ts +26 -6
  118. package/dist/flows/flow-step-runners.js +156 -6
  119. package/dist/flows/flow-success.js +20 -15
  120. package/dist/flows/flow-tools.js +63 -21
  121. package/dist/flows/flows.js +15 -11
  122. package/dist/flows/gate.d.ts +4 -3
  123. package/dist/flows/gate.js +1 -1
  124. package/dist/flows/heal-run.d.ts +0 -8
  125. package/dist/flows/heal-run.js +5 -5
  126. package/dist/flows/heal.js +3 -3
  127. package/dist/flows/predicate-to-expect.d.ts +38 -0
  128. package/dist/flows/predicate-to-expect.js +95 -0
  129. package/dist/flows/replay.d.ts +16 -1
  130. package/dist/flows/replay.js +58 -22
  131. package/dist/flows/role-anchor-nearest.d.ts +6 -0
  132. package/dist/flows/role-anchor-nearest.js +45 -0
  133. package/dist/flows/role-drift-reason.d.ts +20 -0
  134. package/dist/flows/role-drift-reason.js +39 -0
  135. package/dist/flows/server-verify.d.ts +1 -1
  136. package/dist/flows/server-verify.js +5 -5
  137. package/dist/flows/suite-flakes.d.ts +33 -0
  138. package/dist/flows/suite-flakes.js +32 -0
  139. package/dist/flows/verify-change-tools.js +32 -10
  140. package/dist/flows/verify-change-verdict.d.ts +15 -0
  141. package/dist/flows/verify-change-verdict.js +25 -0
  142. package/dist/flows/watch-batcher.d.ts +3 -2
  143. package/dist/honesty/already-true.d.ts +2 -0
  144. package/dist/honesty/already-true.js +31 -0
  145. package/dist/honesty/blind-spots.d.ts +35 -3
  146. package/dist/honesty/blind-spots.js +57 -12
  147. package/dist/honesty/honesty.d.ts +3 -2
  148. package/dist/honesty/honesty.js +4 -4
  149. package/dist/honesty/uncaptured-bodies.js +1 -1
  150. package/dist/honesty/unread-outcome.js +2 -2
  151. package/dist/honesty/verified.d.ts +31 -3
  152. package/dist/honesty/verified.js +64 -8
  153. package/dist/http-server.js +23 -7
  154. package/dist/hunt/hunt-report.d.ts +2 -1
  155. package/dist/hunt/hunt-report.js +2 -2
  156. package/dist/index.d.ts +9 -6
  157. package/dist/index.js +41 -25
  158. package/dist/init/agent-rules.d.ts +20 -1
  159. package/dist/init/agent-rules.js +64 -8
  160. package/dist/init/app-choice.d.ts +20 -0
  161. package/dist/init/app-choice.js +25 -0
  162. package/dist/init/astro-host.d.ts +21 -0
  163. package/dist/init/astro-host.js +27 -0
  164. package/dist/init/astro-patch.d.ts +16 -0
  165. package/dist/init/astro-patch.js +240 -0
  166. package/dist/init/capabilities.js +4 -1
  167. package/dist/init/cra.d.ts +50 -0
  168. package/dist/init/cra.js +106 -0
  169. package/dist/init/cursor.js +39 -4
  170. package/dist/init/desktop-doctor.d.ts +3 -2
  171. package/dist/init/desktop-doctor.js +4 -4
  172. package/dist/init/detect.d.ts +9 -2
  173. package/dist/init/detect.js +24 -5
  174. package/dist/init/mcp-clients.d.ts +81 -0
  175. package/dist/init/mcp-clients.js +248 -0
  176. package/dist/init/mcp-registered.d.ts +12 -0
  177. package/dist/init/mcp-registered.js +14 -0
  178. package/dist/init/mcp.d.ts +27 -2
  179. package/dist/init/mcp.js +40 -4
  180. package/dist/init/next-patch.d.ts +2 -4
  181. package/dist/init/next-patch.js +109 -19
  182. package/dist/init/node-io.js +29 -6
  183. package/dist/init/plan-framework.d.ts +16 -1
  184. package/dist/init/plan-framework.js +166 -8
  185. package/dist/init/plan.d.ts +85 -6
  186. package/dist/init/plan.js +196 -33
  187. package/dist/init/project-id.d.ts +5 -9
  188. package/dist/init/project-id.js +10 -22
  189. package/dist/init/run.d.ts +23 -1
  190. package/dist/init/run.js +338 -47
  191. package/dist/init/slash-command.d.ts +9 -0
  192. package/dist/init/slash-command.js +10 -1
  193. package/dist/init/snippets.d.ts +7 -10
  194. package/dist/init/snippets.js +44 -15
  195. package/dist/init/vite-config.js +1 -1
  196. package/dist/init/windows-quote.d.ts +38 -0
  197. package/dist/init/windows-quote.js +70 -0
  198. package/dist/init/workspace-apps.d.ts +23 -0
  199. package/dist/init/workspace-apps.js +68 -0
  200. package/dist/input/driven-redaction.js +1 -1
  201. package/dist/input/network-detail.js +8 -8
  202. package/dist/input/network-mock-tools.js +2 -2
  203. package/dist/input/network-mock.js +4 -4
  204. package/dist/input/real-input.js +8 -3
  205. package/dist/input/scroll-find.js +2 -2
  206. package/dist/input/scroll-tools.d.ts +0 -5
  207. package/dist/input/scroll-tools.js +11 -4
  208. package/dist/input/viewport-tools.js +2 -2
  209. package/dist/journal/ambient-store.d.ts +0 -2
  210. package/dist/journal/ambient-store.js +1 -1
  211. package/dist/journal/ambient.js +1 -1
  212. package/dist/journal/attach-journal.d.ts +2 -1
  213. package/dist/journal/deviation-report.js +6 -6
  214. package/dist/journal/deviation-service.js +1 -1
  215. package/dist/journal/envelope-store.d.ts +0 -2
  216. package/dist/journal/envelope-store.js +1 -1
  217. package/dist/journal/envelope.d.ts +2 -3
  218. package/dist/journal/envelope.js +3 -3
  219. package/dist/journal/journal-query.js +1 -1
  220. package/dist/journal/journal-recorder.d.ts +2 -1
  221. package/dist/journal/journal-recorder.js +2 -2
  222. package/dist/journal/rollups.js +9 -9
  223. package/dist/journal/session-end.d.ts +2 -1
  224. package/dist/journal/session-journal.js +4 -4
  225. package/dist/license/license.d.ts +0 -14
  226. package/dist/license/license.js +8 -8
  227. package/dist/log.d.ts +10 -0
  228. package/dist/log.js +11 -1
  229. package/dist/mcp/mcp-is-error.d.ts +13 -0
  230. package/dist/mcp/mcp-is-error.js +18 -0
  231. package/dist/mcp/mcp-outage.d.ts +32 -0
  232. package/dist/mcp/mcp-outage.js +55 -0
  233. package/dist/mcp/mcp-proxy.d.ts +134 -0
  234. package/dist/mcp/mcp-proxy.js +720 -0
  235. package/dist/{mcp.d.ts → mcp/mcp.d.ts} +13 -10
  236. package/dist/{mcp.js → mcp/mcp.js} +139 -37
  237. package/dist/mcp/proxy-handshake.d.ts +3 -0
  238. package/dist/mcp/proxy-handshake.js +54 -0
  239. package/dist/mcp/proxy-lifecycle.d.ts +64 -0
  240. package/dist/mcp/proxy-lifecycle.js +70 -0
  241. package/dist/mcp/tool-catalog-cache.d.ts +29 -0
  242. package/dist/mcp/tool-catalog-cache.js +55 -0
  243. package/dist/oracles/propose-consequences.d.ts +2 -1
  244. package/dist/oracles/propose-consequences.js +16 -11
  245. package/dist/oracles/self-instrument.d.ts +2 -1
  246. package/dist/oracles/self-instrument.js +1 -1
  247. package/dist/phenomena/phenomena.js +3 -3
  248. package/dist/platform.d.ts +16 -0
  249. package/dist/platform.js +16 -0
  250. package/dist/pool/browser-pool.d.ts +9 -0
  251. package/dist/pool/browser-pool.js +38 -9
  252. package/dist/pool/playwright-launcher.d.ts +15 -2
  253. package/dist/pool/playwright-launcher.js +21 -2
  254. package/dist/project/fs-port.js +1 -1
  255. package/dist/project/project-tools.js +2 -2
  256. package/dist/runs/build-verification-run.js +1 -1
  257. package/dist/runs/repair-prompt.d.ts +0 -6
  258. package/dist/runs/repair-prompt.js +3 -3
  259. package/dist/runs/risk-classify.js +1 -1
  260. package/dist/runs/run-diff.d.ts +0 -2
  261. package/dist/runs/run-diff.js +3 -3
  262. package/dist/runs/run-tools.js +2 -2
  263. package/dist/runs/verification-sync.js +2 -2
  264. package/dist/runs/verify-http.js +1 -1
  265. package/dist/runs/verify-server.js +1 -1
  266. package/dist/session/attachment-history.d.ts +46 -0
  267. package/dist/session/attachment-history.js +65 -0
  268. package/dist/session/command-timeout.d.ts +11 -3
  269. package/dist/session/command-timeout.js +2 -2
  270. package/dist/session/control-envelope.d.ts +6 -0
  271. package/dist/session/control-envelope.js +7 -1
  272. package/dist/session/dev-server-probe.d.ts +13 -0
  273. package/dist/session/dev-server-probe.js +52 -0
  274. package/dist/session/last-act.d.ts +17 -3
  275. package/dist/session/last-act.js +16 -4
  276. package/dist/session/live-control-tools.js +7 -2
  277. package/dist/session/live-control.js +1 -1
  278. package/dist/session/looks-like-dev-server.d.ts +14 -0
  279. package/dist/session/looks-like-dev-server.js +25 -0
  280. package/dist/session/no-session-diagnosis.d.ts +45 -0
  281. package/dist/session/no-session-diagnosis.js +120 -0
  282. package/dist/session/no-session-watch.d.ts +21 -0
  283. package/dist/session/no-session-watch.js +69 -0
  284. package/dist/session/observed-state.d.ts +4 -0
  285. package/dist/session/observed-state.js +19 -1
  286. package/dist/session/output-budget.js +1 -1
  287. package/dist/session/pending-commands.js +1 -0
  288. package/dist/session/session-health.d.ts +8 -1
  289. package/dist/session/session-health.js +8 -7
  290. package/dist/session/session-info.d.ts +43 -8
  291. package/dist/session/session-info.js +46 -1
  292. package/dist/session/session-lease.d.ts +0 -11
  293. package/dist/session/session-lease.js +1 -1
  294. package/dist/session/session-manager.d.ts +6 -0
  295. package/dist/session/session-manager.js +124 -12
  296. package/dist/session/session-readiness.d.ts +1 -1
  297. package/dist/session/session-readiness.js +6 -1
  298. package/dist/session/session-reaper.js +1 -1
  299. package/dist/session/session-reconnect.d.ts +32 -0
  300. package/dist/session/session-reconnect.js +42 -0
  301. package/dist/session/session.d.ts +15 -2
  302. package/dist/session/session.js +77 -26
  303. package/dist/session/state-select.d.ts +1 -1
  304. package/dist/session/state-select.js +1 -1
  305. package/dist/status-payload.d.ts +19 -0
  306. package/dist/status-payload.js +11 -0
  307. package/dist/telemetry/argument-shape.js +10 -1
  308. package/dist/telemetry/browser-mode.d.ts +27 -0
  309. package/dist/telemetry/browser-mode.js +33 -0
  310. package/dist/telemetry/bug-found.d.ts +17 -21
  311. package/dist/telemetry/bug-found.js +120 -8
  312. package/dist/telemetry/cli-telemetry.d.ts +2 -12
  313. package/dist/telemetry/cli-telemetry.js +27 -4
  314. package/dist/telemetry/crash-cause.d.ts +47 -0
  315. package/dist/telemetry/crash-cause.js +104 -0
  316. package/dist/telemetry/daemon-telemetry.d.ts +38 -1
  317. package/dist/telemetry/daemon-telemetry.js +37 -12
  318. package/dist/telemetry/dev-repo.js +4 -4
  319. package/dist/telemetry/error-class.d.ts +26 -0
  320. package/dist/telemetry/error-class.js +52 -0
  321. package/dist/telemetry/error-fingerprint.js +21 -2
  322. package/dist/telemetry/feedback-cli.d.ts +8 -3
  323. package/dist/telemetry/feedback-cli.js +52 -8
  324. package/dist/telemetry/feedback-context.d.ts +23 -6
  325. package/dist/telemetry/feedback-context.js +105 -5
  326. package/dist/telemetry/feedback-delivery.d.ts +20 -0
  327. package/dist/telemetry/feedback-delivery.js +30 -0
  328. package/dist/telemetry/feedback-outbox.d.ts +22 -0
  329. package/dist/telemetry/feedback-outbox.js +87 -0
  330. package/dist/telemetry/feedback.d.ts +22 -1
  331. package/dist/telemetry/feedback.js +67 -8
  332. package/dist/telemetry/git-facts.d.ts +7 -0
  333. package/dist/telemetry/git-facts.js +5 -5
  334. package/dist/telemetry/project-profile.js +2 -1
  335. package/dist/telemetry/session-metrics.d.ts +47 -3
  336. package/dist/telemetry/session-metrics.js +315 -15
  337. package/dist/telemetry/telemetry.d.ts +45 -2
  338. package/dist/telemetry/telemetry.js +150 -37
  339. package/dist/telemetry/verification-of.d.ts +22 -0
  340. package/dist/telemetry/verification-of.js +84 -0
  341. package/dist/tools/act-capsule.js +2 -2
  342. package/dist/tools/act-danger.d.ts +12 -0
  343. package/dist/tools/act-danger.js +20 -3
  344. package/dist/tools/act-helpers.js +4 -4
  345. package/dist/tools/act-tools.d.ts +0 -8
  346. package/dist/tools/act-tools.js +114 -118
  347. package/dist/tools/act-view.js +7 -7
  348. package/dist/tools/alias-args.d.ts +17 -0
  349. package/dist/tools/alias-args.js +25 -0
  350. package/dist/tools/assert-grade.js +9 -9
  351. package/dist/tools/assert-verdict.d.ts +9 -1
  352. package/dist/tools/assert-verdict.js +21 -6
  353. package/dist/tools/browser-tools.js +36 -11
  354. package/dist/tools/contract-tools.d.ts +0 -5
  355. package/dist/tools/contract-tools.js +27 -1
  356. package/dist/tools/coverage-identity.d.ts +34 -0
  357. package/dist/tools/coverage-identity.js +46 -0
  358. package/dist/tools/coverage-tools.js +10 -14
  359. package/dist/tools/dynamic-tools.d.ts +2 -1
  360. package/dist/tools/dynamic-tools.js +96 -19
  361. package/dist/tools/error-recovery.d.ts +34 -2
  362. package/dist/tools/error-recovery.js +255 -4
  363. package/dist/tools/feedback-invite.d.ts +48 -0
  364. package/dist/tools/feedback-invite.js +71 -0
  365. package/dist/tools/feedback-tools.d.ts +12 -1
  366. package/dist/tools/feedback-tools.js +28 -5
  367. package/dist/tools/invoke-tool.d.ts +0 -12
  368. package/dist/tools/invoke-tool.js +126 -33
  369. package/dist/tools/lease-hint.d.ts +17 -0
  370. package/dist/tools/lease-hint.js +24 -0
  371. package/dist/tools/lease-tools.js +6 -5
  372. package/dist/tools/merge-tools.d.ts +10 -1
  373. package/dist/tools/merge-tools.js +9 -3
  374. package/dist/tools/merged-name-redirect.d.ts +13 -0
  375. package/dist/tools/merged-name-redirect.js +55 -0
  376. package/dist/tools/navigate-arrival.d.ts +26 -0
  377. package/dist/tools/navigate-arrival.js +60 -0
  378. package/dist/tools/navigate-result.d.ts +31 -0
  379. package/dist/tools/navigate-result.js +38 -0
  380. package/dist/tools/observe-tools.js +72 -20
  381. package/dist/tools/observed-nothing.d.ts +25 -0
  382. package/dist/tools/observed-nothing.js +33 -0
  383. package/dist/tools/query-paginate.js +2 -2
  384. package/dist/tools/query-shape.d.ts +18 -0
  385. package/dist/tools/query-shape.js +39 -0
  386. package/dist/tools/read-tools.js +81 -20
  387. package/dist/tools/real-input-attempt.d.ts +28 -0
  388. package/dist/tools/real-input-attempt.js +93 -0
  389. package/dist/tools/reconcile-tools.js +7 -7
  390. package/dist/tools/reload-result.d.ts +14 -0
  391. package/dist/tools/reload-result.js +29 -0
  392. package/dist/tools/snapshot-delta.d.ts +4 -5
  393. package/dist/tools/snapshot-delta.js +0 -0
  394. package/dist/tools/tool-kit.d.ts +16 -0
  395. package/dist/tools/tool-surface.d.ts +83 -0
  396. package/dist/tools/tool-surface.js +217 -0
  397. package/dist/tools/tools-helpers.js +6 -6
  398. package/dist/tools/tools.d.ts +15 -0
  399. package/dist/tools/tools.js +53 -17
  400. package/dist/tools/unadvertised-help.d.ts +5 -0
  401. package/dist/tools/unadvertised-help.js +39 -0
  402. package/dist/trace.d.ts +69 -0
  403. package/dist/trace.js +150 -0
  404. package/dist/update/nudge-credit.d.ts +4 -0
  405. package/dist/update/nudge-credit.js +53 -0
  406. package/dist/update/sdk-sync.d.ts +21 -0
  407. package/dist/update/sdk-sync.js +42 -0
  408. package/dist/update/update-checker.js +3 -3
  409. package/dist/update/update-nudge.d.ts +63 -6
  410. package/dist/update/update-nudge.js +128 -15
  411. package/dist/update/updater.d.ts +1 -9
  412. package/dist/update/updater.js +13 -8
  413. package/dist/version/peer-announce.d.ts +4 -0
  414. package/dist/version/peer-announce.js +30 -0
  415. package/dist/{server-version.js → version/server-version.js} +5 -1
  416. package/dist/version/version-nudge.d.ts +28 -0
  417. package/dist/version/version-nudge.js +40 -0
  418. package/dist/version/version-skew.d.ts +66 -0
  419. package/dist/version/version-skew.js +71 -0
  420. package/dist/visual/visual-diff.js +2 -2
  421. package/dist/visual/visual-tools.js +4 -4
  422. package/docs/README.md +55 -0
  423. package/docs/agent-cheatsheet.md +85 -0
  424. package/docs/architecture.md +113 -0
  425. package/docs/benchmarks.md +255 -0
  426. package/docs/debugging.md +102 -0
  427. package/docs/deploy-checks.md +69 -0
  428. package/docs/desktop-apps.md +256 -0
  429. package/docs/enterprise.md +62 -0
  430. package/docs/fixtures-dispatch-receiver.yml +76 -0
  431. package/docs/fixtures.md +45 -0
  432. package/docs/flows.md +166 -0
  433. package/docs/gate-plan.md +98 -0
  434. package/docs/gates.md +117 -0
  435. package/docs/getting-started.md +469 -0
  436. package/docs/human-control.md +63 -0
  437. package/docs/integration-patterns.md +171 -0
  438. package/docs/local-registry.md +126 -0
  439. package/docs/matrix/2.5.0/claude-code-machine.json +19 -0
  440. package/docs/matrix/2.5.0/codex-machine.json +20 -0
  441. package/docs/matrix/2.5.0/cursor-machine.json +22 -0
  442. package/docs/matrix/2.5.0/gemini-machine.json +22 -0
  443. package/docs/matrix/2.5.0/opencode-machine.json +22 -0
  444. package/docs/matrix/2.5.0/vscode-machine.json +22 -0
  445. package/docs/matrix/2.5.0/windsurf-machine.json +22 -0
  446. package/docs/matrix/MATRIX.md +26 -0
  447. package/docs/matrix/README.md +59 -0
  448. package/docs/mint.json +61 -0
  449. package/docs/multi-agent-testing.md +55 -0
  450. package/docs/platform-integration.md +122 -0
  451. package/docs/system-map.md +152 -0
  452. package/docs/telemetry-contract.md +214 -0
  453. package/docs/telemetry.md +191 -0
  454. package/docs/testing.md +82 -0
  455. package/docs/token-efficiency.md +80 -0
  456. package/docs/usage.md +1046 -0
  457. package/package.json +8 -5
  458. package/dist/cli-doctor.d.ts +0 -6
  459. package/dist/cli-doctor.js +0 -60
  460. package/dist/cli-launch.js +0 -112
  461. package/dist/daemon-resilience.d.ts +0 -22
  462. package/dist/daemon-resilience.js +0 -85
  463. package/dist/mcp-proxy.d.ts +0 -67
  464. package/dist/mcp-proxy.js +0 -337
  465. package/dist/tools/profiles.d.ts +0 -37
  466. package/dist/tools/profiles.js +0 -139
  467. /package/dist/{pairing-token.d.ts → bridge/pairing-token.d.ts} +0 -0
  468. /package/dist/{token-auth.d.ts → bridge/token-auth.d.ts} +0 -0
  469. /package/dist/{token-auth.js → bridge/token-auth.js} +0 -0
  470. /package/dist/{cli-verify.d.ts → cli/cli-verify.d.ts} +0 -0
  471. /package/dist/{cloud-cli.d.ts → cli/cloud-cli.d.ts} +0 -0
  472. /package/dist/{server-version.d.ts → version/server-version.d.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import { isOpaqueOrigin } from '@reticlehq/core';
1
+ import { AppRuntime, isOpaqueOrigin } from '@reticlehq/core';
2
2
  /**
3
3
  * Inbound silence beyond this means the page may genuinely be wedged; inside it, the SDK has spoken
4
4
  * recently enough that "the page is hung" is provably the wrong story. A page that is streaming
@@ -18,7 +18,7 @@ const HIDDEN_ADVICE = 'The page last reported itself hidden, and this is a WKWeb
18
18
  'loaded. If neither fits, the page is reachable but not executing, which is worth reporting.';
19
19
  /** True when this session is a WebKit desktop shell — the only runtime that suffers this. */
20
20
  function isWebKitDesktop(context) {
21
- if (context.runtime === 'tauri')
21
+ if (AppRuntime.TAURI === context.runtime)
22
22
  return true;
23
23
  // A `tauri://` origin is unambiguous even before the first health report lands.
24
24
  if (context.runtime !== undefined)
@@ -12,6 +12,12 @@ interface ControlEnvelope {
12
12
  * recovery path (address the guidance, then reticle_session{action:"resume"}) lives here in exactly one place.
13
13
  */
14
14
  export declare const PAUSE_HINT = "Paused by the human. Address the guidance, then call reticle_session{action:\"resume\"} (or wait for the human to resume).";
15
+ /**
16
+ * `because` for the verdict a paused verification returns. Nothing was driven and nothing was
17
+ * asserted, which is the definition of unknown — never `no`, which would report the human's own
18
+ * pause as the app failing.
19
+ */
20
+ export declare const PAUSED_NO_VERDICT = "the human paused this session, so nothing was driven and nothing was asserted";
15
21
  /** Shape returned by the short-circuit when an action tool refuses while paused. */
16
22
  interface PausedResult {
17
23
  paused: true;
@@ -5,6 +5,12 @@ import { z } from 'zod';
5
5
  * recovery path (address the guidance, then reticle_session{action:"resume"}) lives here in exactly one place.
6
6
  */
7
7
  export const PAUSE_HINT = 'Paused by the human. Address the guidance, then call reticle_session{action:"resume"} (or wait for the human to resume).';
8
+ /**
9
+ * `because` for the verdict a paused verification returns. Nothing was driven and nothing was
10
+ * asserted, which is the definition of unknown — never `no`, which would report the human's own
11
+ * pause as the app failing.
12
+ */
13
+ export const PAUSED_NO_VERDICT = 'the human paused this session, so nothing was driven and nothing was asserted';
8
14
  /**
9
15
  * outputSchema fields every action tool that can short-circuit on pause MUST declare, or a validating
10
16
  * profile strips the WHOLE pause payload — and because `guidance` is drained-once, that loses the
@@ -28,7 +34,7 @@ export const pausedOutputShape = {
28
34
  export function buildControlEnvelope(session) {
29
35
  const state = session.getState();
30
36
  const guidance = session.drainInbox().map((m) => m.text);
31
- if (state === SessionState.ACTIVE && guidance.length === 0)
37
+ if (state === SessionState.ACTIVE && 0 === guidance.length)
32
38
  return undefined;
33
39
  return { state, guidance };
34
40
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Which of the usual dev-server ports have something listening on them.
3
+ *
4
+ * The one fact that separates "the app is not running" from "the app is running and never dialled
5
+ * us" — the difference between two completely different next actions for the agent, which the
6
+ * no-session message could not tell apart before. See no-session-diagnosis.ts.
7
+ *
8
+ * Kept out of the hot path: `SessionManager.resolve` is synchronous and runs on every tool call, so
9
+ * the probe refreshes in the background and the resolve path reads a cached answer. A stale answer
10
+ * is fine here — it is a hint in an error message, not a decision.
11
+ */
12
+ /** Every candidate port serving a document, in ascending order. Never rejects. */
13
+ export declare function probeDevServers(ports?: readonly number[], probe?: (port: number) => Promise<boolean>): Promise<number[]>;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Which of the usual dev-server ports have something listening on them.
3
+ *
4
+ * The one fact that separates "the app is not running" from "the app is running and never dialled
5
+ * us" — the difference between two completely different next actions for the agent, which the
6
+ * no-session message could not tell apart before. See no-session-diagnosis.ts.
7
+ *
8
+ * Kept out of the hot path: `SessionManager.resolve` is synchronous and runs on every tool call, so
9
+ * the probe refreshes in the background and the resolve path reads a cached answer. A stale answer
10
+ * is fine here — it is a hint in an error message, not a decision.
11
+ */
12
+ import { request } from 'node:http';
13
+ import { DEV_SERVER_PORTS } from '../cli/cli-port.js';
14
+ import { looksLikeDevServer } from './looks-like-dev-server.js';
15
+ /** Give up fast: an HTTP answer from localhost either lands quickly or nothing is there. */
16
+ const HTTP_PROBE_TIMEOUT_MS = 400;
17
+ /**
18
+ * `localhost`, not `127.0.0.1` — so BOTH address families are tried.
19
+ *
20
+ * Measured: a plain `vite --port 4311` on this machine listens on `[::1]` only, and a probe pinned to
21
+ * the IPv4 loopback cannot see it. The old TCP probe had the same blind spot, which meant the
22
+ * diagnostic could miss the very dev server it exists to find while still reporting an unrelated
23
+ * service that happens to bind both stacks (macOS AirPlay does).
24
+ */
25
+ const PROBE_HOST = 'localhost';
26
+ /**
27
+ * Does this port serve a DOCUMENT — i.e. is it a dev server rather than merely something listening?
28
+ *
29
+ * A bare TCP connect reported macOS ControlCenter (AirPlay Receiver, on by default on every Mac,
30
+ * port 5000) as the user's dev server. See looks-like-dev-server for what it actually answers and
31
+ * why "a socket accepted" is the wrong question.
32
+ */
33
+ function servesDocument(port, host = PROBE_HOST) {
34
+ return new Promise((resolve) => {
35
+ const req = request({ host, port, path: '/', method: 'GET', timeout: HTTP_PROBE_TIMEOUT_MS }, (res) => {
36
+ const answer = looksLikeDevServer(res.statusCode ?? 0, res.headers['content-type']);
37
+ res.resume(); // drain, so the socket closes rather than lingering
38
+ resolve(answer);
39
+ });
40
+ req.once('timeout', () => {
41
+ req.destroy();
42
+ resolve(false);
43
+ });
44
+ req.once('error', () => resolve(false));
45
+ req.end();
46
+ });
47
+ }
48
+ /** Every candidate port serving a document, in ascending order. Never rejects. */
49
+ export async function probeDevServers(ports = [...DEV_SERVER_PORTS], probe = (p) => servesDocument(p)) {
50
+ const results = await Promise.all(ports.map((p) => probe(p).then((up) => (up ? p : null))));
51
+ return results.filter((p) => p !== null).sort((a, b) => a - b);
52
+ }
@@ -14,18 +14,32 @@
14
14
  * the "this click did nothing" check is unreachable on the ordinary act-then-observe flow: an
15
15
  * empty-window test is a statement about the PAGE, and no real app has a quiet page.
16
16
  */
17
- export interface ActEffect {
17
+ interface ActEffect {
18
18
  action?: string | undefined;
19
19
  /** DOM mutations inside the acted element's own subtree. Undefined means nobody measured. */
20
20
  mutatedWithin?: number | undefined;
21
21
  }
22
22
  export declare class LastAct {
23
23
  #private;
24
- markCursor(cursor: number): void;
24
+ /**
25
+ * Record an action that ACTUALLY DISPATCHED — cursor and effect together, never one without the
26
+ * other.
27
+ *
28
+ * It used to be two setters called BEFORE the ref was resolved, so a refused act (stale ref, a
29
+ * disabled control, a paused page) left a cursor and an empty effect standing. The next observe
30
+ * saw a cursor inside its window, asked for the effect, found `mutatedWithin: undefined` over a
31
+ * quiet window and reported "the click was dispatched and the page settled … the target does not
32
+ * react" — about a click nobody dispatched, blaming a control that was fine, on exactly the
33
+ * re-query-and-retry path the stale-ref message sends the agent down.
34
+ *
35
+ * Marking only on the success path is what makes that unrepeatable: a failure mode added later
36
+ * cannot forget to clean up state that was never written.
37
+ */
38
+ markActed(cursor: number, action: string | undefined, mutatedWithin: number | undefined): void;
25
39
  cursor(): number | undefined;
26
40
  markSource(source: string | undefined): void;
27
41
  source(): string | undefined;
28
- markEffect(action: string | undefined, mutatedWithin: number | undefined): void;
29
42
  /** Empty when nothing has acted — callers then fall back to checks that need no action context. */
30
43
  effect(): ActEffect;
31
44
  }
45
+ export {};
@@ -2,8 +2,23 @@ export class LastAct {
2
2
  #cursor;
3
3
  #source;
4
4
  #effect;
5
- markCursor(cursor) {
5
+ /**
6
+ * Record an action that ACTUALLY DISPATCHED — cursor and effect together, never one without the
7
+ * other.
8
+ *
9
+ * It used to be two setters called BEFORE the ref was resolved, so a refused act (stale ref, a
10
+ * disabled control, a paused page) left a cursor and an empty effect standing. The next observe
11
+ * saw a cursor inside its window, asked for the effect, found `mutatedWithin: undefined` over a
12
+ * quiet window and reported "the click was dispatched and the page settled … the target does not
13
+ * react" — about a click nobody dispatched, blaming a control that was fine, on exactly the
14
+ * re-query-and-retry path the stale-ref message sends the agent down.
15
+ *
16
+ * Marking only on the success path is what makes that unrepeatable: a failure mode added later
17
+ * cannot forget to clean up state that was never written.
18
+ */
19
+ markActed(cursor, action, mutatedWithin) {
6
20
  this.#cursor = cursor;
21
+ this.#effect = { action, mutatedWithin };
7
22
  }
8
23
  cursor() {
9
24
  return this.#cursor;
@@ -14,9 +29,6 @@ export class LastAct {
14
29
  source() {
15
30
  return this.#source;
16
31
  }
17
- markEffect(action, mutatedWithin) {
18
- this.#effect = { action, mutatedWithin };
19
- }
20
32
  /** Empty when nothing has acted — callers then fall back to checks that need no action context. */
21
33
  effect() {
22
34
  return this.#effect ?? {};
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { noteEmptyRead } from '../tools/observed-nothing.js';
2
3
  import { AGENT_ASK_NOTICE, AGENT_WAITING_NOTICE, PresenterTone, SessionState, } from '@reticlehq/core';
3
4
  import { ReticleTool } from '../tools/tool-names.js';
4
5
  import { sessionIdShape } from '../tools/tool-kit.js';
@@ -95,7 +96,11 @@ export const LIVE_CONTROL_TOOLS = [
95
96
  outputSchema: { messages: z.array(z.unknown()) },
96
97
  handler: (deps, args) => {
97
98
  const session = deps.sessions.resolve(asString(args['sessionId']));
98
- return Promise.resolve({ messages: session.drainInbox() });
99
+ // An empty inbox and a panel that is not wired both return `[]`. The first means the human has
100
+ // said nothing; the second means an agent is waiting on a channel that does not exist.
101
+ return Promise.resolve(noteEmptyRead({ messages: session.drainInbox() }, 'messages', {
102
+ noun: 'messages from the human since the last poll',
103
+ }));
99
104
  },
100
105
  },
101
106
  {
@@ -135,7 +140,7 @@ export const LIVE_CONTROL_TOOLS = [
135
140
  session.pushNarration(`✓ fixed: ${mark.note}`);
136
141
  }
137
142
  }
138
- const source = args['all'] === true ? session.allMarks() : session.pendingMarks();
143
+ const source = true === args['all'] ? session.allMarks() : session.pendingMarks();
139
144
  const marks = source.map((m) => ({ ...m, fix: buildFixHint(m) }));
140
145
  const out = {
141
146
  marks,
@@ -27,7 +27,7 @@ export class LiveControl {
27
27
  /** Queue a human note. Empty/whitespace-only text is ignored so a stray enter cannot spam the agent. */
28
28
  push(text, t) {
29
29
  const trimmed = text.trim();
30
- if (trimmed.length === 0)
30
+ if (0 === trimmed.length)
31
31
  return;
32
32
  this.#inbox.push({ text: trimmed, t });
33
33
  }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Is the thing on this port a DEV SERVER serving the app, or just something on a port?
3
+ *
4
+ * The probe used to answer "something accepted a TCP connection", which on macOS means every Mac
5
+ * reports Apple's AirPlay Receiver on port 5000 as the user's dev server. Reproduced here: it
6
+ * answers `HTTP/1.1 403 Forbidden`, `Server: AirTunes/950.7.1`, no body. The agent was then told
7
+ * "something IS listening on port 5000 and this project is wired for Reticle" while the app was on
8
+ * 3100 — a diagnostic confidently naming a service with nothing to do with the project.
9
+ *
10
+ * A dev server serving an app answers `GET /` with a document. That is the whole test. A false
11
+ * negative costs a less specific message; a false positive costs trust in every diagnostic Reticle
12
+ * prints, which is far more expensive.
13
+ */
14
+ export declare function looksLikeDevServer(status: number, contentType: string | undefined): boolean;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Is the thing on this port a DEV SERVER serving the app, or just something on a port?
3
+ *
4
+ * The probe used to answer "something accepted a TCP connection", which on macOS means every Mac
5
+ * reports Apple's AirPlay Receiver on port 5000 as the user's dev server. Reproduced here: it
6
+ * answers `HTTP/1.1 403 Forbidden`, `Server: AirTunes/950.7.1`, no body. The agent was then told
7
+ * "something IS listening on port 5000 and this project is wired for Reticle" while the app was on
8
+ * 3100 — a diagnostic confidently naming a service with nothing to do with the project.
9
+ *
10
+ * A dev server serving an app answers `GET /` with a document. That is the whole test. A false
11
+ * negative costs a less specific message; a false positive costs trust in every diagnostic Reticle
12
+ * prints, which is far more expensive.
13
+ */
14
+ /** Statuses that mean "I will not serve you", which no local dev server does for its own app root. */
15
+ const REFUSES = new Set([401, 403, 407]);
16
+ export function looksLikeDevServer(status, contentType) {
17
+ if (REFUSES.has(status))
18
+ return false;
19
+ // A redirect IS a server routing: a dev server on a base path answers `/` with one.
20
+ if (status >= 300 && status < 400)
21
+ return true;
22
+ // Otherwise it has to be serving a document. A 404 that renders HTML is still a dev server — the
23
+ // server is there, only the route is missing.
24
+ return contentType !== undefined && contentType.toLowerCase().includes('text/html');
25
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Turning "no browser session connected" from a dead end into a next action.
3
+ *
4
+ * This is the most consequential sentence in the product. Measured over a day of telemetry: 74% of
5
+ * sessions never call a Reticle tool, and of the ones that do, half make exactly ONE call — usually
6
+ * `reticle_sessions` — and stop. Ten of those thirteen never touched a browser, and every recorded
7
+ * session error is this one. The agent asks whether anything is connected, is told no, and leaves.
8
+ *
9
+ * The old message asked the agent to check two things it cannot see from where it stands ("is your
10
+ * app running with the SDK enabled?", "does it point at this port?"). The daemon can actually tell
11
+ * the three cases apart, and each has a different, concrete fix:
12
+ *
13
+ * - a session was here and went away -> the tab closed or reloaded; reopen it
14
+ * - something is listening, never dialled, project not wired -> run `reticle init` in THAT app
15
+ * - something is listening, project wired -> port mismatch or a stale build; restart the dev server
16
+ * - nothing is listening anywhere -> there is no app running; start it
17
+ *
18
+ * Pure: everything it needs is passed in, so the probe that discovers listening ports stays out of
19
+ * the hot resolve() path and this stays unit-testable.
20
+ */
21
+ interface NoSessionFacts {
22
+ /** Whether ANY session has connected to this daemon since it booted. */
23
+ everConnected: boolean;
24
+ /** Whether this project has been through `reticle init` (a .reticle.json / projectId is present). */
25
+ initialized: boolean;
26
+ /** Localhost ports with something listening that looks like a dev server. */
27
+ listening: readonly number[];
28
+ /** The port this daemon is on — half of the mismatch the old message asked about. */
29
+ port: number;
30
+ /**
31
+ * This daemon has reaped at least one EXPIRED pooled lease.
32
+ *
33
+ * Deliberately not "the session that just vanished was that lease" — nothing knows that. It is
34
+ * evidence, not proof, and the message below is worded accordingly.
35
+ */
36
+ leaseExpired?: boolean;
37
+ }
38
+ /**
39
+ * Every branch ends with this, and `recoveryFor` keys on it to suppress the generic no-session
40
+ * recovery: a message that already carries its own next action must not be handed a second, more
41
+ * generic one that contradicts it.
42
+ */
43
+ export declare const SELF_RECOVERING_MARKER = "Then call reticle_sessions again \u2014 it will appear within a second of the page loading.";
44
+ export declare function diagnoseNoSession(facts: NoSessionFacts): string;
45
+ export {};
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Turning "no browser session connected" from a dead end into a next action.
3
+ *
4
+ * This is the most consequential sentence in the product. Measured over a day of telemetry: 74% of
5
+ * sessions never call a Reticle tool, and of the ones that do, half make exactly ONE call — usually
6
+ * `reticle_sessions` — and stop. Ten of those thirteen never touched a browser, and every recorded
7
+ * session error is this one. The agent asks whether anything is connected, is told no, and leaves.
8
+ *
9
+ * The old message asked the agent to check two things it cannot see from where it stands ("is your
10
+ * app running with the SDK enabled?", "does it point at this port?"). The daemon can actually tell
11
+ * the three cases apart, and each has a different, concrete fix:
12
+ *
13
+ * - a session was here and went away -> the tab closed or reloaded; reopen it
14
+ * - something is listening, never dialled, project not wired -> run `reticle init` in THAT app
15
+ * - something is listening, project wired -> port mismatch or a stale build; restart the dev server
16
+ * - nothing is listening anywhere -> there is no app running; start it
17
+ *
18
+ * Pure: everything it needs is passed in, so the probe that discovers listening ports stays out of
19
+ * the hot resolve() path and this stays unit-testable.
20
+ */
21
+ import { DEV_SERVER_PORTS } from '../cli/cli-port.js';
22
+ /**
23
+ * Every branch ends with this, and `recoveryFor` keys on it to suppress the generic no-session
24
+ * recovery: a message that already carries its own next action must not be handed a second, more
25
+ * generic one that contradicts it.
26
+ */
27
+ export const SELF_RECOVERING_MARKER = 'Then call reticle_sessions again — it will appear within a second of the page loading.';
28
+ const RETRY = SELF_RECOVERING_MARKER;
29
+ /**
30
+ * The way out that needs no human at all.
31
+ *
32
+ * `reticle_lease` opens a browser Reticle drives itself, instead of waiting for somebody's tab to
33
+ * dial in. Measured over a day of telemetry it is the single strongest predictor of a session that
34
+ * works: the 5 sessions that used it had a MEDIAN of 30 tool calls and produced 46% of every bug
35
+ * found, against a median of 1 call for the 20 active sessions that did not — and not one
36
+ * single-call bounce used one. It is also advertised on no profile except `full`, so an agent only
37
+ * ever finds it if it already knew it existed. Naming it HERE puts it in front of the agent at the
38
+ * one moment it is the answer, and costs nothing on the turns when it is not.
39
+ *
40
+ * Only offered when the app is known to carry the SDK: leasing an uninstrumented app just burns a
41
+ * browser and comes back `ready:false`.
42
+ */
43
+ const SELF_SERVE = 'You do not have to wait for the human: reticle_lease {action:"acquire", url} opens a browser ' +
44
+ 'Reticle drives itself, and returns a sessionId you can use immediately (reach it with ' +
45
+ 'reticle_run {tool:"reticle_lease"} if it is not advertised directly; release it when you finish).';
46
+ /**
47
+ * The ports the scan actually covers, rendered for the message.
48
+ *
49
+ * Derived from DEV_SERVER_PORTS rather than re-typed: a message that lists ports the scan does not
50
+ * check, or omits ones it does, is a new version of the same defect — a confident claim about
51
+ * evidence that was never gathered.
52
+ */
53
+ /**
54
+ * The second half of the answer for a project that never went through `init`.
55
+ *
56
+ * Held separately because it must appear ONLY when the project is uninstrumented — telling a wired
57
+ * project to re-run `init` sends the reader back to a step that already succeeded, which is its own
58
+ * kind of wrong answer.
59
+ */
60
+ const UNINSTRUMENTED = 'Separately: this project has not been through `reticle init`, which is what installs the SDK ' +
61
+ 'and wires it into the build — so even once the dev server is up, the app will carry no SDK and ' +
62
+ "no session will appear. Run `reticle init` in the app's directory too.";
63
+ const SCANNED_PORTS = [...DEV_SERVER_PORTS].join(', ');
64
+ export function diagnoseNoSession(facts) {
65
+ const { everConnected, initialized, listening, port } = facts;
66
+ const ports = listening.join(', ');
67
+ if (everConnected) {
68
+ // A reaped lease first, because it is the one cause we have POSITIVE evidence for. Reported
69
+ // from the field (#157): an aged-out lease produced "the tab was closed … ask the human to
70
+ // reopen the app", which is wrong on every clause — there is no human tab, and the recovery it
71
+ // names is unavailable to the caller while the one that works goes unmentioned. The reporter
72
+ // went looking for a port mismatch. Hedged rather than swapped: a human tab may ALSO have
73
+ // closed, and this does not know which session went.
74
+ if (true === facts.leaseExpired) {
75
+ return ('no browser session connected — but one WAS connected to this daemon earlier, so the wiring ' +
76
+ 'is correct. This daemon has expired at least one pooled lease, so the likeliest cause is ' +
77
+ 'that a lease you were using aged out; a lease is a headless context, not a human tab, and ' +
78
+ 'it takes its cookies with it (so an authenticated app needs signing in again). Re-acquire ' +
79
+ 'with reticle_lease {action:"acquire", url} and carry on. If you were driving a human tab ' +
80
+ `instead, it went away — reopen it or run \`reticle open\`. ${RETRY}`);
81
+ }
82
+ return ('no browser session connected — but one WAS connected to this daemon earlier, so the wiring ' +
83
+ 'is correct. The tab was closed, navigated away, or hard-reloaded. Ask the human to reopen ' +
84
+ `the app (or run \`reticle open\`), or reload the tab. ${SELF_SERVE} ${RETRY}`);
85
+ }
86
+ if (0 === listening.length) {
87
+ return ('no browser session connected, and nothing is listening on the ports Reticle scans ' +
88
+ `(${SCANNED_PORTS}). The likeliest cause by far is that the dev server is not running: ask ` +
89
+ 'the human to start it (`npm run dev`), then open the app in a browser. ' +
90
+ // The caveat is here rather than omitted because the scan is NARROW, and the old sentence
91
+ // spent its confidence as though an empty result from eleven ports were proof of absence.
92
+ // Reported from a scripted drive of 2.5.0: this branch asserted the app was not running while
93
+ // it served 200 on :7699. A dev server on any other port — 4173 from `vite preview`, 1420
94
+ // from Tauri, 5175 from a second Vite, anything passed to --port — is invisible to it.
95
+ 'That scan is narrow, so it is not proof: a server on any other port is invisible to it. If ' +
96
+ // Deliberately NOT offering reticle_lease here, and a test pins that: a lease opens a URL, and
97
+ // if nothing is listening there is nothing at any URL to open. Asking for the real one is the
98
+ // only move that can recover the :7699 case.
99
+ 'the app IS running, ask the human for its URL rather than assuming it is down. ' +
100
+ // BOTH facts at once when the project was never wired. This branch fires before the
101
+ // `!initialized` one, so an uninstrumented project used to be told only "start your dev
102
+ // server" — the reader starts it, calls again, and is told ONLY THEN that no app carries the
103
+ // SDK. Two round trips to learn two things the daemon knew on the first call, and this is the
104
+ // largest cohort in the funnel (#171): 77 users attached an agent and never instrumented an
105
+ // app. They have a daemon (they registered the MCP server) and no SDK anywhere.
106
+ `${initialized ? '' : UNINSTRUMENTED} ${RETRY}`);
107
+ }
108
+ if (!initialized) {
109
+ return (`no browser session connected, but something IS listening on port ${ports} — so a server is ` +
110
+ 'up and has never dialled this daemon. This project has not been through `reticle init`, ' +
111
+ 'which is what installs the SDK and wires it into the build, so the likeliest cause is that ' +
112
+ "the app carries no Reticle SDK. Ask the human to run `reticle init` in the app's directory " +
113
+ `and restart the dev server. ${RETRY}`);
114
+ }
115
+ return (`no browser session connected, but something IS listening on port ${ports} and this project is ` +
116
+ `wired for Reticle — so the app is either serving a build made before the wiring landed, or ` +
117
+ `dialling a different daemon than this one (this daemon is on ${String(port)}). Ask the human ` +
118
+ 'to restart the dev server and hard-reload the page; if it still does not appear, check that ' +
119
+ `the app's reticle port matches ${String(port)}. ${SELF_SERVE} ${RETRY}`);
120
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Keeps the "why is nothing connected" diagnosis fresh, without putting a probe on the hot path.
3
+ *
4
+ * `SessionManager.resolve` is synchronous and runs on every tool call, so it cannot await a port
5
+ * scan. This refreshes the answer in the background and hands the manager a closure that reads the
6
+ * cached result — a hint in an error message is exactly the sort of thing that may be a few seconds
7
+ * stale.
8
+ *
9
+ * It only probes while NOTHING is connected. Once a session is live the question is moot, and a
10
+ * daemon that outlives the agent by hours has no business scanning ports it does not need.
11
+ */
12
+ import type { SessionManager } from './session-manager.js';
13
+ /**
14
+ * The daemon's whole session-scoping decision in one call: scope auto-selection to the active
15
+ * project, and keep the no-session diagnosis fresh. Both derive from the same one fact — whether
16
+ * this directory has been through `reticle init` — so they belong together rather than as two
17
+ * adjacent blocks in the bootstrap.
18
+ */
19
+ export declare function wireSessionScope(sessions: SessionManager, activeProjectId: string | undefined, port: number,
20
+ /** Reader for the pool's aged-out-lease count; omitted when this daemon runs no pool. */
21
+ reapedLeases?: () => number): () => void;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Keeps the "why is nothing connected" diagnosis fresh, without putting a probe on the hot path.
3
+ *
4
+ * `SessionManager.resolve` is synchronous and runs on every tool call, so it cannot await a port
5
+ * scan. This refreshes the answer in the background and hands the manager a closure that reads the
6
+ * cached result — a hint in an error message is exactly the sort of thing that may be a few seconds
7
+ * stale.
8
+ *
9
+ * It only probes while NOTHING is connected. Once a session is live the question is moot, and a
10
+ * daemon that outlives the agent by hours has no business scanning ports it does not need.
11
+ */
12
+ import { probeDevServers } from './dev-server-probe.js';
13
+ import { diagnoseNoSession } from './no-session-diagnosis.js';
14
+ /** Slow enough to be free, fast enough that a dev server started 15s ago is already reflected. */
15
+ const REFRESH_MS = 15_000;
16
+ /** Start the watch. Returns a stop function; the timer is unref'd so it never holds the daemon up. */
17
+ function startNoSessionWatch(options) {
18
+ const probe = options.probe ?? (() => probeDevServers());
19
+ let listening = [];
20
+ let running = false;
21
+ const refresh = () => {
22
+ // Nothing to diagnose while a session is live, and no reason to scan.
23
+ if (running || options.sessions.count() > 0)
24
+ return;
25
+ running = true;
26
+ void probe()
27
+ .then((ports) => {
28
+ listening = ports;
29
+ })
30
+ .catch(() => {
31
+ /* a diagnostic hint must never take the daemon down */
32
+ })
33
+ .finally(() => {
34
+ running = false;
35
+ });
36
+ };
37
+ refresh();
38
+ const timer = setInterval(refresh, REFRESH_MS);
39
+ timer.unref();
40
+ options.sessions.setNoSessionHint(() => diagnoseNoSession({
41
+ everConnected: options.sessions.everConnected(),
42
+ initialized: options.initialized,
43
+ listening,
44
+ port: options.port,
45
+ leaseExpired: (options.reapedLeases?.() ?? 0) > 0,
46
+ }));
47
+ return () => {
48
+ clearInterval(timer);
49
+ options.sessions.setNoSessionHint(undefined);
50
+ };
51
+ }
52
+ /**
53
+ * The daemon's whole session-scoping decision in one call: scope auto-selection to the active
54
+ * project, and keep the no-session diagnosis fresh. Both derive from the same one fact — whether
55
+ * this directory has been through `reticle init` — so they belong together rather than as two
56
+ * adjacent blocks in the bootstrap.
57
+ */
58
+ export function wireSessionScope(sessions, activeProjectId, port,
59
+ /** Reader for the pool's aged-out-lease count; omitted when this daemon runs no pool. */
60
+ reapedLeases) {
61
+ if (activeProjectId !== undefined)
62
+ sessions.setDefaultScope({ projectId: activeProjectId });
63
+ return startNoSessionWatch({
64
+ sessions,
65
+ port,
66
+ initialized: activeProjectId !== undefined,
67
+ ...(reapedLeases === undefined ? {} : { reapedLeases }),
68
+ });
69
+ }
@@ -42,8 +42,12 @@ export declare class ObservedState {
42
42
  seedAmbient(counts: AmbientCounts): void;
43
43
  /** Record a driven ref. Idempotent. */
44
44
  recordActedRef(ref: string): void;
45
+ /** Record a driven control's re-render-surviving label. Idempotent; empty labels are not identities. */
46
+ recordActedLabel(label: string): void;
45
47
  /** Every ref driven so far this session. */
46
48
  actedRefs(): ReadonlySet<string>;
49
+ /** Every driven control's label, for matching across re-renders. */
50
+ actedLabels(): ReadonlySet<string>;
47
51
  /**
48
52
  * Record bridge-side sampling as a blind spot.
49
53
  *
@@ -34,7 +34,7 @@ export class ObservedState {
34
34
  if (event.type === EventType.BLIND_SPOT) {
35
35
  const kind = event.data['kind'];
36
36
  const count = event.data['count'];
37
- if (typeof kind === 'string' && typeof count === 'number')
37
+ if ('string' === typeof kind && 'number' === typeof count)
38
38
  this.#blindSpots[kind] = count;
39
39
  }
40
40
  }
@@ -78,15 +78,33 @@ export class ObservedState {
78
78
  * "recently untouched" — a number that reads as thorough while drifting toward the opposite.
79
79
  */
80
80
  #actedRefs = new Set();
81
+ /**
82
+ * The same drives, keyed by something that SURVIVES a re-render.
83
+ *
84
+ * A ref is invalidated whenever the DOM changes, so on a framework that replaces nodes rather than
85
+ * reconciling in place (Next's app router, for one) every driven ref is stale by the time coverage
86
+ * is asked and the number reads 0 forever. The snapshot label — `button "Deploy"` — names the same
87
+ * control on both sides of a re-render.
88
+ */
89
+ #actedLabels = new Set();
81
90
  /** Record a driven ref. Idempotent. */
82
91
  recordActedRef(ref) {
83
92
  if (ref.length > 0)
84
93
  this.#actedRefs.add(ref);
85
94
  }
95
+ /** Record a driven control's re-render-surviving label. Idempotent; empty labels are not identities. */
96
+ recordActedLabel(label) {
97
+ if (label.length > 0)
98
+ this.#actedLabels.add(label);
99
+ }
86
100
  /** Every ref driven so far this session. */
87
101
  actedRefs() {
88
102
  return this.#actedRefs;
89
103
  }
104
+ /** Every driven control's label, for matching across re-renders. */
105
+ actedLabels() {
106
+ return this.#actedLabels;
107
+ }
90
108
  /**
91
109
  * Record bridge-side sampling as a blind spot.
92
110
  *
@@ -58,7 +58,7 @@ export function sizeCost(payload) {
58
58
  * Non-object results (e.g. a thrown-error envelope) pass through unchanged.
59
59
  */
60
60
  export function withSizeCost(result) {
61
- if (typeof result !== 'object' || result === null)
61
+ if (typeof result !== 'object' || null === result)
62
62
  return result;
63
63
  return { ...result, cost: sizeCost(result) };
64
64
  }
@@ -13,6 +13,7 @@ export class PendingCommands {
13
13
  this.#pending.delete(id);
14
14
  reject(new Error(describeTimeout()));
15
15
  }, timeoutMs);
16
+ timer.unref();
16
17
  this.#pending.set(id, { resolve, reject, timer });
17
18
  });
18
19
  }
@@ -1,3 +1,4 @@
1
+ import { BrowserBrand } from '@reticlehq/core';
1
2
  import type { Session } from './session.js';
2
3
  /**
3
4
  * The health block spliced onto act/assert results. Defined here rather than in session.ts: this is
@@ -45,12 +46,18 @@ export declare function healthEnvelope(session: Session): HealthEnvelope;
45
46
  */
46
47
  export declare function refuseIfThrottled(session: Session, refuse: unknown): void;
47
48
  /** The page's own visibility/runtime report, narrowed out of an untrusted PAGE_HEALTH payload. */
48
- export interface HealthReport {
49
+ interface HealthReport {
49
50
  hidden: boolean | undefined;
50
51
  focused: boolean | undefined;
51
52
  runtime: string | undefined;
52
53
  /** Coarse rendering engine (blink/gecko/webkit) — context for a feedback report, not a health input. */
53
54
  engine: string | undefined;
55
+ /**
56
+ * Which browser the page is. Narrowed to `BrowserBrand` HERE rather than trusted: the SDK already
57
+ * normalises, so anything else is a stale or hand-forged payload and is dropped — the point of the
58
+ * closed list is that an unbounded string can never reach telemetry through it.
59
+ */
60
+ brand: BrowserBrand | undefined;
54
61
  }
55
62
  /**
56
63
  * Narrow a PAGE_HEALTH payload. Every field is optional on the wire — an older SDK does not report