@reticlehq/server 3.1.0 → 3.3.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 (384) hide show
  1. package/SKILL.md +30 -28
  2. package/dist/command/cli/adhoc-suite.d.ts +19 -0
  3. package/dist/command/cli/adhoc-suite.js +88 -0
  4. package/dist/command/cli/adhoc-verdict.d.ts +23 -2
  5. package/dist/command/cli/adhoc-verdict.js +18 -10
  6. package/dist/command/cli/auth/device-flow.d.ts +2 -2
  7. package/dist/command/cli/cli-doctor.js +37 -5
  8. package/dist/command/cli/cli-flow-commands.d.ts +2 -1
  9. package/dist/command/cli/cli-kill.js +1 -1
  10. package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
  11. package/dist/command/cli/cli-parse-grammar.js +7 -0
  12. package/dist/command/cli/cli-parse-verify.d.ts +2 -5
  13. package/dist/command/cli/cli-parse-verify.js +86 -13
  14. package/dist/command/cli/cli-parse.d.ts +8 -1
  15. package/dist/command/cli/cli-parse.js +38 -7
  16. package/dist/command/cli/cli-verify.d.ts +59 -0
  17. package/dist/command/cli/cli-verify.js +131 -19
  18. package/dist/command/cli/cloud-cli.js +7 -6
  19. package/dist/command/cli/cloud-kit.d.ts +0 -2
  20. package/dist/command/cli/cloud-kit.js +3 -7
  21. package/dist/command/cli/config/config-discovery.d.ts +12 -0
  22. package/dist/command/cli/config/config-discovery.js +19 -0
  23. package/dist/command/cli/doctor/doctor-rows.d.ts +12 -0
  24. package/dist/command/cli/doctor/doctor-rows.js +12 -0
  25. package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
  26. package/dist/command/cli/drive/drive-command.js +1 -1
  27. package/dist/command/cli/launch/cli-launch.d.ts +3 -23
  28. package/dist/command/cli/launch/cli-launch.js +29 -70
  29. package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +33 -0
  30. package/dist/command/cli/lifecycle/daemon-lifecycle.js +198 -0
  31. package/dist/command/cli/mcp-command.js +5 -3
  32. package/dist/command/cli/ports/resolve/cli-port.d.ts +15 -1
  33. package/dist/command/cli/ports/resolve/cli-port.js +50 -2
  34. package/dist/command/cli/report-command.d.ts +16 -0
  35. package/dist/command/cli/report-command.js +97 -0
  36. package/dist/command/cli/status/status-lines.d.ts +28 -0
  37. package/dist/command/cli/status/status-lines.js +117 -0
  38. package/dist/command/cli.d.ts +1 -1
  39. package/dist/command/cli.js +29 -177
  40. package/dist/command/daemon/binding/daemon-status-probe.d.ts +32 -0
  41. package/dist/command/daemon/binding/daemon-status-probe.js +93 -0
  42. package/dist/command/daemon/binding/loopback-reach.d.ts +42 -0
  43. package/dist/command/daemon/binding/loopback-reach.js +63 -0
  44. package/dist/command/daemon/binding/port-presence.d.ts +1 -1
  45. package/dist/command/daemon/daemon-resilience.d.ts +13 -1
  46. package/dist/command/daemon/daemon-resilience.js +12 -1
  47. package/dist/command/daemon/daemon.d.ts +2 -11
  48. package/dist/command/daemon/daemon.js +8 -11
  49. package/dist/command/daemon/idle-shutdown.d.ts +2 -1
  50. package/dist/command/daemon/lifetime/heartbeat.d.ts +8 -0
  51. package/dist/command/daemon/lifetime/heartbeat.js +10 -0
  52. package/dist/command/daemon/lifetime/log-cap.d.ts +68 -0
  53. package/dist/command/daemon/lifetime/log-cap.js +115 -0
  54. package/dist/command/setup/agent-notice-memory.d.ts +16 -0
  55. package/dist/command/setup/agent-notice-memory.js +65 -0
  56. package/dist/command/setup/bringup/dev-server-wait.d.ts +1 -0
  57. package/dist/command/setup/bringup/dev-server-wait.js +16 -0
  58. package/dist/command/setup/bringup/ensure-daemon.js +1 -1
  59. package/dist/command/setup/init/init-host.js +8 -0
  60. package/dist/command/setup/init/init-runtime.js +13 -1
  61. package/dist/command/setup/node-effects.js +1 -1
  62. package/dist/command/setup/probe/page-probe.js +9 -2
  63. package/dist/command/setup/run-setup.d.ts +25 -7
  64. package/dist/command/setup/run-setup.js +51 -9
  65. package/dist/command/setup/setup-command.js +31 -3
  66. package/dist/command/setup/terminal/confirm.js +2 -1
  67. package/dist/command/update/updater.d.ts +27 -0
  68. package/dist/command/update/updater.js +35 -2
  69. package/dist/features/crawl/crawl-tools.js +3 -1
  70. package/dist/features/crawl/crawl.d.ts +2 -0
  71. package/dist/features/crawl/crawl.js +3 -1
  72. package/dist/features/harness/drive-report.d.ts +73 -0
  73. package/dist/features/harness/drive-report.js +196 -0
  74. package/dist/features/harness/driver.js +29 -8
  75. package/dist/features/harness/drivers.d.ts +19 -0
  76. package/dist/features/harness/drivers.js +19 -0
  77. package/dist/features/harness/fill-values.d.ts +67 -0
  78. package/dist/features/harness/fill-values.js +153 -0
  79. package/dist/features/harness/jev-driver.d.ts +188 -0
  80. package/dist/features/harness/jev-driver.js +661 -0
  81. package/dist/features/harness/openai-driver.d.ts +46 -0
  82. package/dist/features/harness/openai-driver.js +197 -0
  83. package/dist/features/harness/platform-config.d.ts +62 -0
  84. package/dist/features/harness/platform-config.js +97 -0
  85. package/dist/features/visual/visual-tools.js +26 -2
  86. package/dist/index.d.ts +1 -1
  87. package/dist/index.js +80 -130
  88. package/dist/judgement/capsule/capsule-store.d.ts +31 -2
  89. package/dist/judgement/capsule/capsule-store.js +58 -6
  90. package/dist/judgement/capsule/capsule.js +5 -0
  91. package/dist/judgement/capsule/causal-summary.d.ts +13 -1
  92. package/dist/judgement/capsule/causal-summary.js +7 -0
  93. package/dist/judgement/domain/domain-model.js +38 -9
  94. package/dist/judgement/domain/domain-tools.js +4 -2
  95. package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
  96. package/dist/judgement/phenomena/phenomena.d.ts +2 -1
  97. package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
  98. package/dist/judgement/runs/artifact/gap-report.js +46 -0
  99. package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
  100. package/dist/judgement/runs/artifact/gap-summary.js +85 -0
  101. package/dist/judgement/runs/artifact/render-report.js +4 -1
  102. package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
  103. package/dist/judgement/runs/artifact/to-artifact.js +1 -1
  104. package/dist/judgement/runs/context-tools.js +21 -3
  105. package/dist/judgement/runs/drive-run.js +7 -2
  106. package/dist/judgement/runs/verification-sync.d.ts +2 -2
  107. package/dist/judgement/runs/verification-sync.js +9 -2
  108. package/dist/language/flows/affected-tools.js +6 -3
  109. package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
  110. package/dist/language/flows/annotate-notes/annotate.js +66 -18
  111. package/dist/language/flows/flow-anchor.d.ts +12 -0
  112. package/dist/language/flows/flow-anchor.js +20 -0
  113. package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
  114. package/dist/language/flows/flow-expect-grammar.js +92 -11
  115. package/dist/language/flows/flow-intent.js +1 -1
  116. package/dist/language/flows/flow-learning.d.ts +3 -0
  117. package/dist/language/flows/flow-learning.js +39 -8
  118. package/dist/language/flows/flow-replay-run.d.ts +55 -10
  119. package/dist/language/flows/flow-replay-run.js +217 -58
  120. package/dist/language/flows/flow-replay-types.d.ts +9 -0
  121. package/dist/language/flows/flow-replay.d.ts +29 -11
  122. package/dist/language/flows/flow-replay.js +221 -203
  123. package/dist/language/flows/flow-result.d.ts +18 -1
  124. package/dist/language/flows/flow-result.js +18 -2
  125. package/dist/language/flows/flow-step-runners.d.ts +26 -10
  126. package/dist/language/flows/flow-step-runners.js +20 -4
  127. package/dist/language/flows/flow-store-for-session.d.ts +2 -1
  128. package/dist/language/flows/flow-success.d.ts +21 -6
  129. package/dist/language/flows/flow-success.js +50 -138
  130. package/dist/language/flows/flow-tools.js +8 -4
  131. package/dist/language/flows/flows.d.ts +15 -10
  132. package/dist/language/flows/flows.js +17 -4
  133. package/dist/language/flows/heal.d.ts +9 -1
  134. package/dist/language/flows/heal.js +24 -2
  135. package/dist/language/flows/learned-sources.d.ts +21 -0
  136. package/dist/language/flows/learned-sources.js +84 -0
  137. package/dist/language/flows/narrow-write.d.ts +4 -4
  138. package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
  139. package/dist/language/flows/recording/recording-backtrack.js +1 -1
  140. package/dist/language/flows/recording/replay-halt.d.ts +2 -1
  141. package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
  142. package/dist/language/flows/replay.js +13 -9
  143. package/dist/language/flows/stores/annotation-store.d.ts +5 -5
  144. package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
  145. package/dist/language/flows/stores/flake-store.js +2 -3
  146. package/dist/language/flows/suite/coverage.d.ts +1 -1
  147. package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
  148. package/dist/language/flows/suite-selection.d.ts +15 -1
  149. package/dist/language/flows/suite-selection.js +1 -1
  150. package/dist/language/flows/verify-change-tools.js +21 -4
  151. package/dist/memory/cloud/account-state.d.ts +1 -1
  152. package/dist/memory/cloud/account-state.js +5 -4
  153. package/dist/memory/cloud/cloud-config.d.ts +9 -0
  154. package/dist/memory/cloud/cloud-config.js +9 -0
  155. package/dist/memory/cloud/cloud-sync.d.ts +7 -4
  156. package/dist/memory/cloud/cloud-sync.js +11 -6
  157. package/dist/memory/cloud/harness-config.d.ts +12 -0
  158. package/dist/memory/cloud/harness-config.js +58 -0
  159. package/dist/memory/cloud/harness-offer.d.ts +44 -0
  160. package/dist/memory/cloud/harness-offer.js +146 -0
  161. package/dist/memory/cloud/harness-switch.d.ts +11 -0
  162. package/dist/memory/cloud/harness-switch.js +53 -0
  163. package/dist/memory/cloud/sync-cycle.d.ts +12 -0
  164. package/dist/memory/cloud/sync-cycle.js +29 -4
  165. package/dist/memory/cloud/sync-daemon.d.ts +35 -2
  166. package/dist/memory/cloud/sync-daemon.js +45 -0
  167. package/dist/memory/cloud/sync-disk.js +11 -6
  168. package/dist/memory/impact/impact-recorder.d.ts +8 -0
  169. package/dist/memory/impact/impact-recorder.js +2 -1
  170. package/dist/memory/impact/impact-store.d.ts +5 -0
  171. package/dist/memory/impact/impact-store.js +25 -0
  172. package/dist/memory/intent/inline-intent.js +8 -3
  173. package/dist/memory/intent/intent-shard-store.d.ts +33 -47
  174. package/dist/memory/intent/intent-shard-store.js +253 -141
  175. package/dist/memory/intent/intent-shard.d.ts +22 -23
  176. package/dist/memory/intent/intent-shard.js +1 -1
  177. package/dist/memory/intent/intent-store.d.ts +13 -8
  178. package/dist/memory/intent/intent-store.js +46 -90
  179. package/dist/memory/intent/intent-tools.js +34 -23
  180. package/dist/memory/journal/ambient-store.js +2 -3
  181. package/dist/memory/journal/attach-journal.d.ts +11 -0
  182. package/dist/memory/journal/attach-journal.js +1 -1
  183. package/dist/memory/journal/deviation-report.js +4 -1
  184. package/dist/memory/journal/deviation-service.js +10 -2
  185. package/dist/memory/journal/drive-flow.d.ts +2 -2
  186. package/dist/memory/journal/drive-flow.js +21 -12
  187. package/dist/memory/journal/envelope-store.js +2 -3
  188. package/dist/memory/journal/journal-query.d.ts +35 -1
  189. package/dist/memory/journal/journal-query.js +46 -0
  190. package/dist/memory/journal/journal-recorder.d.ts +71 -2
  191. package/dist/memory/journal/journal-recorder.js +45 -7
  192. package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
  193. package/dist/memory/journal/on-disk/retain-policy.js +44 -0
  194. package/dist/memory/journal/on-disk/retention.d.ts +91 -2
  195. package/dist/memory/journal/on-disk/retention.js +181 -11
  196. package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
  197. package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
  198. package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -2
  199. package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -38
  200. package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
  201. package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
  202. package/dist/memory/journal/route-template.d.ts +22 -0
  203. package/dist/memory/journal/route-template.js +44 -0
  204. package/dist/memory/journal/self-observation.d.ts +3 -0
  205. package/dist/memory/journal/self-observation.js +67 -0
  206. package/dist/memory/journal/session-end.d.ts +17 -4
  207. package/dist/memory/journal/session-end.js +85 -7
  208. package/dist/memory/journal/session-journal.d.ts +122 -5
  209. package/dist/memory/journal/session-journal.js +489 -32
  210. package/dist/memory/project/artifact-root-resolver.d.ts +3 -0
  211. package/dist/memory/project/artifact-root-resolver.js +89 -0
  212. package/dist/memory/project/artifact-root.d.ts +39 -2
  213. package/dist/memory/project/artifact-root.js +58 -1
  214. package/dist/memory/project/baselines.d.ts +1 -1
  215. package/dist/memory/project/dir/fill-value-store.d.ts +32 -0
  216. package/dist/memory/project/dir/fill-value-store.js +88 -0
  217. package/dist/memory/project/dir/reticle-dir.d.ts +15 -5
  218. package/dist/memory/project/dir/reticle-dir.js +11 -0
  219. package/dist/memory/project/fs/fs-port.d.ts +22 -1
  220. package/dist/memory/project/fs/fs-port.js +72 -5
  221. package/dist/memory/project/fs/write-atomic.d.ts +26 -0
  222. package/dist/memory/project/fs/write-atomic.js +48 -0
  223. package/dist/memory/project/learned-routes.d.ts +16 -2
  224. package/dist/memory/project/learned-routes.js +26 -10
  225. package/dist/memory/project/project-for-root.d.ts +30 -0
  226. package/dist/memory/project/project-for-root.js +43 -0
  227. package/dist/memory/project/project-tools.js +10 -5
  228. package/dist/memory/project/session-root.d.ts +35 -6
  229. package/dist/memory/project/session-root.js +70 -8
  230. package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
  231. package/dist/memory/recall/prior/connection-memory.js +8 -0
  232. package/dist/memory/recall/registered-projects.d.ts +2 -1
  233. package/dist/portal/bridge/auth-failure-reason.d.ts +9 -0
  234. package/dist/portal/bridge/auth-failure-reason.js +20 -3
  235. package/dist/portal/bridge/bridge.d.ts +6 -1
  236. package/dist/portal/bridge/bridge.js +36 -2
  237. package/dist/portal/input/real-input.js +2 -1
  238. package/dist/portal/pool/browser-pool.d.ts +10 -0
  239. package/dist/portal/pool/browser-pool.js +10 -0
  240. package/dist/portal/pool/playwright-launcher.js +1 -0
  241. package/dist/portal/pool/pool-contract.d.ts +2 -0
  242. package/dist/portal/pool/storage-seed.d.ts +2 -1
  243. package/dist/portal/pool/zero-install.d.ts +27 -0
  244. package/dist/portal/pool/zero-install.js +54 -0
  245. package/dist/portal/realm/web-realm.js +6 -0
  246. package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
  247. package/dist/portal/session/dev-server/served-document.d.ts +21 -0
  248. package/dist/portal/session/dev-server/served-document.js +57 -0
  249. package/dist/portal/session/facts/background-traffic.d.ts +9 -0
  250. package/dist/portal/session/facts/background-traffic.js +25 -0
  251. package/dist/portal/session/facts/session-replaced.d.ts +19 -0
  252. package/dist/portal/session/facts/session-replaced.js +22 -0
  253. package/dist/portal/session/live-control-tools.js +48 -3
  254. package/dist/portal/session/no-session-diagnosis.d.ts +1 -0
  255. package/dist/portal/session/no-session-diagnosis.js +67 -60
  256. package/dist/portal/session/no-session-next-action.js +3 -2
  257. package/dist/portal/session/no-session-watch.js +14 -10
  258. package/dist/portal/session/presence/attachment-history.d.ts +13 -2
  259. package/dist/portal/session/rebind.d.ts +18 -0
  260. package/dist/portal/session/rebind.js +38 -0
  261. package/dist/portal/session/session-health.d.ts +61 -0
  262. package/dist/portal/session/session-health.js +40 -1
  263. package/dist/portal/session/session-info.d.ts +6 -0
  264. package/dist/portal/session/session-manager.d.ts +13 -0
  265. package/dist/portal/session/session-manager.js +42 -3
  266. package/dist/portal/session/session-successor.d.ts +11 -0
  267. package/dist/portal/session/session-successor.js +14 -1
  268. package/dist/portal/session/session-verdict-facts.d.ts +28 -0
  269. package/dist/portal/session/session-verdict-facts.js +15 -0
  270. package/dist/portal/session/session.d.ts +26 -10
  271. package/dist/portal/session/session.js +48 -49
  272. package/dist/status-payload.d.ts +12 -1
  273. package/dist/status-payload.js +4 -1
  274. package/dist/surface/http-server.js +3 -5
  275. package/dist/surface/mcp/faults/mcp-outage.d.ts +2 -1
  276. package/dist/surface/mcp/faults/mcp-outage.js +20 -4
  277. package/dist/surface/mcp/mcp-proxy.d.ts +10 -1
  278. package/dist/surface/mcp/mcp-proxy.js +23 -12
  279. package/dist/surface/mcp/mcp.d.ts +10 -1
  280. package/dist/surface/mcp/mcp.js +59 -29
  281. package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +15 -2
  282. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +22 -5
  283. package/dist/surface/mcp/proxy/proxy-log.d.ts +5 -11
  284. package/dist/surface/mcp/proxy/proxy-log.js +7 -22
  285. package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
  286. package/dist/surface/mcp/server-instructions.js +12 -1
  287. package/dist/surface/tools/act/act-preflight.js +6 -1
  288. package/dist/surface/tools/act/already-true.d.ts +40 -0
  289. package/dist/surface/tools/act/already-true.js +36 -0
  290. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
  291. package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
  292. package/dist/surface/tools/act/sequence-grade.js +13 -0
  293. package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
  294. package/dist/surface/tools/act/settle-in-flight.js +29 -6
  295. package/dist/surface/tools/act-capsule.js +49 -8
  296. package/dist/surface/tools/act-tools.js +25 -24
  297. package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
  298. package/dist/surface/tools/args/numeric-bounds.js +30 -3
  299. package/dist/surface/tools/assert/assert-grade.js +14 -2
  300. package/dist/surface/tools/assert/assert-verdict.js +74 -21
  301. package/dist/surface/tools/browser-tools.js +4 -3
  302. package/dist/surface/tools/coverage-tools.js +5 -2
  303. package/dist/surface/tools/dynamic-tools.js +43 -46
  304. package/dist/surface/tools/error-recovery.d.ts +6 -0
  305. package/dist/surface/tools/error-recovery.js +15 -0
  306. package/dist/surface/tools/explore-tools.js +80 -7
  307. package/dist/surface/tools/feedback-tools.d.ts +2 -1
  308. package/dist/surface/tools/feedback-tools.js +50 -3
  309. package/dist/surface/tools/first-sentence.d.ts +24 -0
  310. package/dist/surface/tools/first-sentence.js +0 -0
  311. package/dist/surface/tools/harness-explore.d.ts +125 -1
  312. package/dist/surface/tools/harness-explore.js +401 -15
  313. package/dist/surface/tools/harness-plan.d.ts +76 -0
  314. package/dist/surface/tools/harness-plan.js +156 -0
  315. package/dist/surface/tools/harness-toolset.js +12 -1
  316. package/dist/surface/tools/invoke-tool.js +82 -9
  317. package/dist/surface/tools/lease-hint.d.ts +8 -0
  318. package/dist/surface/tools/lease-hint.js +10 -0
  319. package/dist/surface/tools/lease-tools.d.ts +16 -28
  320. package/dist/surface/tools/lease-tools.js +53 -5
  321. package/dist/surface/tools/lineage-tools.js +1 -1
  322. package/dist/surface/tools/live-call-text.d.ts +6 -0
  323. package/dist/surface/tools/live-call-text.js +36 -2
  324. package/dist/surface/tools/observe-tools.js +69 -15
  325. package/dist/surface/tools/read/query-windowed.d.ts +1 -1
  326. package/dist/surface/tools/read/query-windowed.js +1 -1
  327. package/dist/surface/tools/read-tools.js +1 -2
  328. package/dist/surface/tools/real-input-attempt.d.ts +0 -5
  329. package/dist/surface/tools/real-input-attempt.js +1 -1
  330. package/dist/surface/tools/tool-kit.d.ts +23 -3
  331. package/dist/surface/tools/tool-kit.js +6 -1
  332. package/dist/surface/tools/tool-surface.js +17 -5
  333. package/dist/surface/tools/tools-helpers.d.ts +8 -7
  334. package/dist/surface/tools/tools-helpers.js +11 -13
  335. package/dist/surface/tools/tools.js +70 -30
  336. package/dist/telemetry/bug-found.js +32 -2
  337. package/dist/telemetry/feedback-context.js +3 -3
  338. package/dist/telemetry/feedback-local.d.ts +1 -0
  339. package/dist/telemetry/feedback-local.js +1 -3
  340. package/dist/telemetry/feedback.d.ts +1 -1
  341. package/dist/telemetry/harness-drive.d.ts +6 -0
  342. package/dist/telemetry/harness-drive.js +39 -0
  343. package/dist/telemetry/hud-metrics.d.ts +32 -0
  344. package/dist/telemetry/hud-metrics.js +95 -0
  345. package/dist/telemetry/mcp-connection.d.ts +14 -1
  346. package/dist/telemetry/mcp-connection.js +17 -2
  347. package/dist/telemetry/onboarding-firsts.d.ts +30 -1
  348. package/dist/telemetry/onboarding-firsts.js +54 -12
  349. package/dist/telemetry/session-metrics.d.ts +5 -0
  350. package/dist/telemetry/session-metrics.js +15 -1
  351. package/dist/telemetry/verification-of.js +31 -3
  352. package/dist/wire-journal.d.ts +31 -0
  353. package/dist/wire-journal.js +100 -0
  354. package/docs/README.md +1 -0
  355. package/docs/agent-cheatsheet.md +7 -3
  356. package/docs/autodrive.md +42 -2
  357. package/docs/benchmarks.md +1 -1
  358. package/docs/capabilities.mdx +1 -1
  359. package/docs/cli/cloud.mdx +11 -9
  360. package/docs/cli/doctor.mdx +1 -0
  361. package/docs/cli/init.mdx +1 -0
  362. package/docs/cli/report.mdx +44 -0
  363. package/docs/cli/status.mdx +35 -20
  364. package/docs/cli.mdx +8 -18
  365. package/docs/desktop.mdx +1 -1
  366. package/docs/faq.mdx +1 -1
  367. package/docs/flows.md +11 -0
  368. package/docs/frameworks.mdx +113 -1
  369. package/docs/install-agentic.mdx +36 -0
  370. package/docs/install-manual.mdx +1 -1
  371. package/docs/packages/tauri.mdx +1 -1
  372. package/docs/packages.mdx +1 -1
  373. package/docs/predicates.mdx +144 -4
  374. package/docs/system-map.md +6 -4
  375. package/docs/telemetry.md +2 -1
  376. package/docs/token-efficiency.md +2 -2
  377. package/docs/tools/overview.mdx +1 -1
  378. package/docs/tools/query.mdx +10 -10
  379. package/docs/troubleshooting.mdx +8 -2
  380. package/docs/usage.md +2 -2
  381. package/docs/what-is-recorded.md +62 -0
  382. package/package.json +5 -5
  383. package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
  384. package/dist/judgement/outcome/predicate-to-expect.js +0 -148
package/dist/index.js CHANGED
@@ -1,14 +1,17 @@
1
+ import { writeHarnessSwitch } from './memory/cloud/harness-switch.js';
2
+ import { harnessConfigSource } from './memory/cloud/harness-config.js';
3
+ import { fetchPlatformConfig } from './features/harness/platform-config.js';
1
4
  import { join } from 'node:path';
2
5
  import { existsSync, readFileSync } from 'node:fs';
3
6
  import { homedir } from 'node:os';
4
- import { resolveProjectCloud } from './memory/cloud/cloud-config.js';
5
- import { startSyncDaemon } from './memory/cloud/sync-daemon.js';
7
+ import { linkedCloudPort } from './memory/cloud/cloud-config.js';
8
+ import { attachCloudSync } from './memory/cloud/sync-daemon.js';
6
9
  import { wireHooks } from './hooks/hook-commands.js';
7
- import { PROJECT_REGISTRY_FILE, emptyProjectRegistry, parseProjectRegistry, projectCandidates, } from '@reticlehq/core/artifacts';
8
- import { discoverProjectConfigs, } from './command/cli/config/config-discovery.js';
9
- import { projectCandidatesFrom, resolveArtifactRoot, } from './memory/project/artifact-root.js';
10
+ import { PROJECT_REGISTRY_FILE, parseProjectRegistry, projectCandidates, } from '@reticlehq/core/artifacts';
11
+ import { discoverProjectConfigs } from './command/cli/config/config-discovery.js';
12
+ import { artifactRootResolver } from './memory/project/artifact-root-resolver.js';
10
13
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
- import { AGENT_STOPPED_NOTICE, RETICLE_DEFAULT_PORT, ReticleCommand, ReticleDir, ReticleEnv, LOOPBACK_HOST, ReplayStatus, EventType, } from '@reticlehq/core';
14
+ import { AGENT_STOPPED_NOTICE, RETICLE_DEFAULT_PORT, bridgeWsUrl, ReticleCommand, ReticleDir, ReticleEnv, LOOPBACK_HOST, ReplayStatus, EventType, } from '@reticlehq/core';
12
15
  import { originOf } from './portal/session/session-manager.js';
13
16
  import { setBrowserMode, BrowserMode } from './telemetry/browser-mode.js';
14
17
  import { replayAndLearn } from './language/flows/flow-learning.js';
@@ -25,6 +28,7 @@ import { initImpact } from './memory/impact/impact-recorder.js';
25
28
  import { FlowStore } from './language/flows/flows.js';
26
29
  import { buildFlowChips } from './language/flows/flow-scope.js';
27
30
  import { ProjectStore } from './memory/project/project-store.js';
31
+ import { projectStoreResolver } from './memory/project/project-for-root.js';
28
32
  import { attachRouteLearning } from './memory/project/learned-routes.js';
29
33
  import { AnnotationStore } from './language/flows/stores/annotation-store.js';
30
34
  import { createNodeFileSystem } from './memory/project/fs/fs-port.js';
@@ -47,19 +51,16 @@ import { BrowserPool } from './portal/pool/browser-pool.js';
47
51
  import { AGENT_ALREADY_DRIVING_ELSEWHERE, shouldGreetWithLeaseNotice, } from './portal/session/lease-visibility.js';
48
52
  import { playwrightLauncher, resolveMaxContexts } from './portal/pool/playwright-launcher.js';
49
53
  import { LeaseReaper } from './portal/pool/lease-reaper.js';
50
- import { readJournalEnabled, readProjectId } from './command/cli/ports/resolve/cli-port.js';
51
- import { hasProjectConnectedBefore } from './memory/recall/prior/connection-memory.js';
54
+ import { findProjectConfig, projectIdsAt, readJournalEnabled, readProjectId, } from './command/cli/ports/resolve/cli-port.js';
55
+ import { hasAnyProjectConnectedBefore } from './memory/recall/prior/connection-memory.js';
52
56
  import { reticleStateHome } from './command/daemon/daemon.js';
53
57
  import { probeChromium } from './command/cli/doctor/browser/chromium-hint.js';
54
- import { makeJournalAttach } from './memory/journal/attach-journal.js';
55
- import { makeSessionEnd, recordDriveRun } from './memory/journal/session-end.js';
56
- import { attachDriveRunFlush } from './memory/journal/drive-run-flush.js';
58
+ import { attachJournal } from './wire-journal.js';
57
59
  import { reportOnboardingStep } from './telemetry/onboarding-funnel.js';
58
60
  import { AMBIENT_RECORDING } from './language/flows/recording/tape/recordings.js';
59
- import { AmbientStore } from './memory/journal/ambient-store.js';
60
- import { ensureWorkspaceGitignore } from './memory/journal/on-disk/workspace-gitignore.js';
61
- import { pruneFeedback, pruneSessions, pruneVisualDiffs, } from './memory/journal/on-disk/retention.js';
61
+ import { readRetainPolicy } from './memory/journal/on-disk/retain-policy.js';
62
62
  import { log } from './log.js';
63
+ import { readReaderBundle, zeroInstallScript as zeroInstallScriptFor, } from './portal/pool/zero-install.js';
63
64
  /** A human-facing one-liner for a panel replay verdict — ✓ passed / ⚠ drifted / ✗ errored. */
64
65
  function replayVerdictLine(result) {
65
66
  if (result.status === ReplayStatus.OK)
@@ -82,7 +83,7 @@ export { runTool, SESSION_BOUND_TOOLS, SESSION_EXEMPT_TOOLS } from './surface/to
82
83
  export { BaselineStore, normalizeLines, diffLines } from './memory/project/baselines.js';
83
84
  export { RecordingStore } from './language/flows/recording/tape/recordings.js';
84
85
  export { FlowStore, recordedStepToFlowStep } from './language/flows/flows.js';
85
- export { assertSuccess, successToPredicate, dynamicTestids, successLabel, } from './language/flows/flow-success.js';
86
+ export { assertSuccess, dynamicTestids, successLabel, successToPredicate, } from './language/flows/flow-success.js';
86
87
  export { classifyFlowAssertions, FlowAssertionGrade } from './language/flows/flow-classify.js';
87
88
  export { buildDomainModel } from './judgement/domain/domain-model.js';
88
89
  export { ProjectStore } from './memory/project/project-store.js';
@@ -104,12 +105,29 @@ export { resolveBridgeSecurity } from './portal/bridge/bridge-security.js';
104
105
  * Build the shared browser pool (one headless Chromium, N capped isolated leased contexts). Lazy —
105
106
  * no Chromium launches until the first lease — so creating it is free even when never used.
106
107
  */
107
- function createBrowserPool(headless) {
108
+ /** Where the zero-install reader connects: this daemon's own bridge, with its pairing token. */
109
+ function readerConnect(port, token) {
110
+ return { url: bridgeWsUrl(port), ...(token === undefined ? {} : { token }) };
111
+ }
112
+ function createBrowserPool(headless, reader) {
108
113
  const maxContexts = resolveMaxContexts(process.env[ReticleEnv.MAX_CONTEXTS], cpus().length);
109
114
  const genSessionId = () => `lease-${'function' === typeof globalThis.crypto?.randomUUID
110
115
  ? globalThis.crypto.randomUUID()
111
116
  : String(Date.now())}`;
112
- return new BrowserPool(playwrightLauncher({ headless }), { maxContexts, genSessionId });
117
+ // Built on first use and kept: the page that needs it is one whose app never connected.
118
+ let script;
119
+ const zeroInstallScript = () => {
120
+ if (script === undefined) {
121
+ const bundle = readReaderBundle();
122
+ script = bundle === undefined ? null : zeroInstallScriptFor(bundle, reader);
123
+ }
124
+ return script ?? undefined;
125
+ };
126
+ return new BrowserPool(playwrightLauncher({ headless }), {
127
+ maxContexts,
128
+ genSessionId,
129
+ zeroInstallScript,
130
+ });
113
131
  }
114
132
  /**
115
133
  * Route CDP-authoritative network detail onto the driven session's journal.
@@ -149,70 +167,6 @@ function makeNetworkDetailRouter(bridge, driveUrl) {
149
167
  }
150
168
  };
151
169
  }
152
- /**
153
- * Wire journal capture, ambient seeding and the journal-tail flush onto a bridge.
154
- *
155
- * Both entry points need all three, and both used to hand-roll them. `startDaemon` only ever wired the
156
- * first, so on the path every user actually takes (`reticle serve` / `reticle mcp`) the journal tail was
157
- * dropped at session end and the learned ambient map was never persisted OR seeded — meaning ambient
158
- * learning could not converge across sessions and the last events of every session were lost. The two
159
- * call sites had already drifted once before, which is why this is one function rather than a
160
- * copy-paste both are asked to keep in step.
161
- */
162
- function attachJournal(bridge, deps) {
163
- const journalAttach = makeJournalAttach(deps);
164
- const ambientStore = new AmbientStore(deps.fs, deps.reticleRoot);
165
- // Built once, not per session: the resolver walks config discovery and the user-level registry,
166
- // and neither changes between two tabs connecting a second apart.
167
- const resolveArtifactRoot = artifactRootResolver(deps.reticleRoot);
168
- bridge.attachSessionCreate((session) => {
169
- // Stamp the project's own `.reticle` before ANY counter fires for this session. Without it every
170
- // verdict is recorded against wherever the daemon was started, which is how one app's evidence
171
- // reached a different account's production dashboard.
172
- session.artifactRoot = resolveArtifactRoot(session.projectId).root;
173
- journalAttach(session);
174
- // Seed the learned ambient map so a fresh session starts knowing which regions churn, instead of
175
- // re-learning from zero. Best-effort + async: a late seed still helps, a failure is silent.
176
- if (deps.enabled) {
177
- void ambientStore
178
- .load()
179
- .then((counts) => session.seedAmbient(counts))
180
- .catch(() => undefined);
181
- }
182
- });
183
- // Teardown: flush the journal tail to disk + persist what this session learned.
184
- bridge.attachSessionEnd(makeSessionEnd(deps));
185
- /*
186
- * And the same write, DURING the session rather than only at the end of it.
187
- *
188
- * Teardown was the only writer of a run artifact, so every verdict produced while a tab stayed
189
- * open was invisible to cloud sync — which can only push artifacts that exist. Reported as "sync
190
- * is not happening" and diagnosed as the agent forgetting to sync; there is no sync command to
191
- * forget. The evidence simply was not on disk yet.
192
- *
193
- * Safe to call repeatedly because the run id is derived from the session, so this rewrites one
194
- * artifact rather than accumulating them — a property `recordDriveRun` already had, for the
195
- * unrelated reason that a reconnecting tab must not publish two overlapping rows.
196
- */
197
- if (deps.enabled) {
198
- attachDriveRunFlush({
199
- resolve: (sessionId) => bridge.sessions.get(sessionId),
200
- write: (session) => recordDriveRun(deps, session),
201
- });
202
- }
203
- if (deps.enabled) {
204
- void pruneSessions(deps.fs, deps.reticleRoot);
205
- // The largest thing in the workspace, and until now the only one with no delete path at all.
206
- void pruneVisualDiffs(deps.fs, deps.reticleRoot);
207
- // Write-only local copies of reports the outbox already carries.
208
- void pruneFeedback(deps.fs, deps.reticleRoot);
209
- // Here rather than in `init`, because this is the moment we are actually about to write into
210
- // somebody's repository — and the paths that reach it without ever running `init` (a plugin
211
- // install, a hand-added client config) are exactly the ones that would otherwise leave an
212
- // unexplained pile of untracked files behind. Best-effort and write-once; see the helper.
213
- void ensureWorkspaceGitignore(deps.fs, deps.reticleRoot);
214
- }
215
- }
216
170
  /**
217
171
  * Resolve the drive/real-input provider from options — shared by start and startDaemon so the
218
172
  * precedence (driveUrl launch+own → CDP attach → none) and the storageState/injectConnect plumbing
@@ -309,39 +263,18 @@ function knownProjectRoots() {
309
263
  }
310
264
  return [...roots];
311
265
  }
312
- function artifactRootResolver(daemonRoot) {
313
- return (projectId) => {
314
- let registry = emptyProjectRegistry();
315
- try {
316
- const path = join(homedir(), ReticleDir.ROOT, PROJECT_REGISTRY_FILE);
317
- registry = existsSync(path)
318
- ? parseProjectRegistry(JSON.parse(readFileSync(path, 'utf8')))
319
- : registry;
320
- }
321
- catch {
322
- // A cache that cannot be read is an empty cache, never an error: the daemon still resolves
323
- // through discovery, and falls back to its own root exactly as it did before this existed.
324
- }
325
- let discovery = { found: [], searched: [] };
326
- try {
327
- discovery = discoverProjectConfigs(process.cwd());
328
- }
329
- catch {
330
- // Same reasoning: a diagnostic search that throws must not take a tool call with it.
331
- }
332
- return resolveArtifactRoot({
333
- projectId,
334
- candidates: projectCandidatesFrom(discovery, registry),
335
- daemonRoot,
336
- });
337
- };
338
- }
339
266
  export async function start(options = {}) {
340
267
  const port = options.port ?? RETICLE_DEFAULT_PORT;
341
268
  // Open the user's impact record before anything can connect. Not inside the MCP branch: a daemon
342
269
  // serving a browser with no agent attached still has a HUD to answer, and a report that reads
343
270
  // "nothing recorded yet" over a month of history on disk is the worst version of this feature.
344
- initImpact({ reticleRoot: options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT) });
271
+ initImpact({
272
+ reticleRoot: options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT),
273
+ // The daemon owns both sides of this seam, so it is the layer that may join them: the cache
274
+ // lives in cloud memory, the platform read lives in the harness feature, and neither is allowed
275
+ // to reach for the other.
276
+ config: harnessConfigSource(() => fetchPlatformConfig(process.env)),
277
+ });
345
278
  const uninstallHooks = wireHooks(options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT), readProjectId(process.cwd()));
346
279
  const security = await resolveBridgeSecurityWithAutoToken(options);
347
280
  const bridge = new Bridge({ port, sdkFix: sdkFixForCwd, ...security });
@@ -369,21 +302,25 @@ export async function start(options = {}) {
369
302
  const fs = createNodeFileSystem();
370
303
  const reticleRoot = options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT);
371
304
  const now = options.now ?? (() => Date.now());
372
- const journalEnabled = readJournalEnabled(process.cwd(), process.env[ReticleEnv.JOURNAL]);
305
+ const retain = readRetainPolicy(findProjectConfig(process.cwd()));
306
+ // `retain.sessions: 0` says keep no journals. Honoured at CAPTURE as well as at the sweep —
307
+ // writing a directory in order to delete it on the way out is work nobody asked for.
308
+ const journalEnabled = readJournalEnabled(process.cwd(), process.env[ReticleEnv.JOURNAL]) && 0 < retain.sessions;
373
309
  const flows = new FlowStore(fs, reticleRoot, { now });
374
310
  attachJournal(bridge, {
375
311
  fs,
376
312
  reticleRoot,
377
313
  enabled: journalEnabled,
314
+ retain,
378
315
  // Bound HERE, where the recorder is already in hand: teardown gets the tape, not the recorder.
379
316
  takeAmbientTape: () => recordings.stop(AMBIENT_RECORDING),
380
317
  reportStep: reportOnboardingStep,
381
318
  flows,
382
319
  });
383
320
  const project = new ProjectStore(fs, reticleRoot, { now });
384
- attachRouteLearning(bridge, project);
321
+ attachRouteLearning(bridge, projectStoreResolver(fs, project, reticleRoot, now));
385
322
  const annotations = new AnnotationStore();
386
- pool = createBrowserPool(options.headless ?? true);
323
+ pool = createBrowserPool(options.headless ?? true, readerConnect(port, security.token));
387
324
  leaseReaper = new LeaseReaper(pool);
388
325
  leaseReaper.start();
389
326
  /*
@@ -424,7 +361,7 @@ export async function start(options = {}) {
424
361
  ...(activeProjectId === undefined ? {} : { projectId: activeProjectId }),
425
362
  };
426
363
  const profile = resolveToolSurface(options.toolProfile);
427
- const server = createMcpServer(realInput !== undefined ? { ...deps, realInput } : deps, profile, hasProjectConnectedBefore(reticleStateHome(), port, activeProjectId));
364
+ const server = createMcpServer(realInput !== undefined ? { ...deps, realInput } : deps, profile, hasAnyProjectConnectedBefore(reticleStateHome(), port, projectIdsAt(process.cwd())));
428
365
  // When the agent (the MCP client) disconnects cleanly, end every active session at once so the
429
366
  // HUD doesn't linger. (If the agent instead KILLS this process, the WS dies and the browser
430
367
  // self-ends via SESSION_LIFECYCLE.BRIDGE_LOST_MS — see transport.ts.)
@@ -462,7 +399,13 @@ export async function startDaemon(options = {}) {
462
399
  // was never opened in the process the HUD talks to: tool calls still recorded (the dispatch
463
400
  // chokepoint opens it lazily), but a tab that connected before the first tool call was pushed
464
401
  // nothing, so the report read "nothing recorded yet" over a file with history in it.
465
- initImpact({ reticleRoot: options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT) });
402
+ initImpact({
403
+ reticleRoot: options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT),
404
+ // The daemon owns both sides of this seam, so it is the layer that may join them: the cache
405
+ // lives in cloud memory, the platform read lives in the harness feature, and neither is allowed
406
+ // to reach for the other.
407
+ config: harnessConfigSource(() => fetchPlatformConfig(process.env)),
408
+ });
466
409
  const security = await resolveBridgeSecurityWithAutoToken(options);
467
410
  const shared = createSharedServer(security.token === undefined ? {} : { token: security.token });
468
411
  const bridge = new Bridge({ port, server: shared.httpServer, sdkFix: sdkFixForCwd, ...security });
@@ -477,7 +420,7 @@ export async function startDaemon(options = {}) {
477
420
  // answers WHY rather than just "sessionCount: 0".
478
421
  // `verifyPort` rides along so a later `serve --http` can tell whether this daemon already honours
479
422
  // the requested `--http-port` instead of silently ignoring the flag (#687).
480
- shared.attachStatus(() => statusPayload(bridge.sessions.count(), bridge.sessions.list(), bridge.sessions.noSessionHint(), verifyHttp?.port));
423
+ shared.attachStatus(() => statusPayload(bridge.sessions.count(), bridge.sessions.list(), bridge.sessions.noSessionHint(), verifyHttp?.port, bridge.sessions.noSessionLead()));
481
424
  // Agent-independent presence: the daemon outlives any single agent, so when the LAST agent's MCP
482
425
  // connection drops (it stopped, or is waiting on the human), end every session and push a clear
483
426
  // "go to your terminal" notice to the panel — the human is on the browser and must not lose a typed
@@ -496,7 +439,9 @@ export async function startDaemon(options = {}) {
496
439
  const fs = createNodeFileSystem();
497
440
  const reticleRoot = options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT);
498
441
  const now = options.now ?? (() => Date.now());
499
- const journalEnabled = readJournalEnabled(process.cwd(), process.env[ReticleEnv.JOURNAL]);
442
+ const retain = readRetainPolicy(findProjectConfig(process.cwd()));
443
+ // See the sibling path: `retain.sessions: 0` stops the capture too, not only the sweep.
444
+ const journalEnabled = readJournalEnabled(process.cwd(), process.env[ReticleEnv.JOURNAL]) && 0 < retain.sessions;
500
445
  const flows = new FlowStore(fs, reticleRoot, { now });
501
446
  // Built here rather than inside `deps` below, so teardown can save what a drive recorded. Both
502
447
  // paths pass the same pair — `daemon-parity.test.ts` is what keeps them from drifting apart.
@@ -504,7 +449,7 @@ export async function startDaemon(options = {}) {
504
449
  /*
505
450
  * Bound AFTER the sync daemon exists, read only when a run is actually written.
506
451
  *
507
- * `attachJournal` runs before `startSyncDaemon` because the journal has to be capturing before
452
+ * `attachJournal` runs before `attachCloudSync` because the journal has to be capturing before
508
453
  * anything can connect, and reordering them so this could be a direct reference would put sync
509
454
  * setup ahead of session capture for the sake of one callback.
510
455
  */
@@ -513,15 +458,16 @@ export async function startDaemon(options = {}) {
513
458
  fs,
514
459
  reticleRoot,
515
460
  enabled: journalEnabled,
461
+ retain,
516
462
  takeAmbientTape: () => recordings.stop(AMBIENT_RECORDING),
517
463
  reportStep: reportOnboardingStep,
518
464
  flows,
519
465
  onRunPersisted: () => syncNudge.run?.(),
520
466
  });
521
467
  const project = new ProjectStore(fs, reticleRoot, { now });
522
- attachRouteLearning(bridge, project);
468
+ attachRouteLearning(bridge, projectStoreResolver(fs, project, reticleRoot, now));
523
469
  const annotations = new AnnotationStore();
524
- const pool = createBrowserPool(options.headless ?? true);
470
+ const pool = createBrowserPool(options.headless ?? true, readerConnect(port, security.token));
525
471
  const leaseReaper = new LeaseReaper(pool);
526
472
  leaseReaper.start();
527
473
  /*
@@ -540,19 +486,22 @@ export async function startDaemon(options = {}) {
540
486
  * events a user most wants: the ones at the start of a run.
541
487
  */
542
488
  wireHooks(reticleRoot, readProjectId(process.cwd()));
543
- const cloudSync = startSyncDaemon({
489
+ const cloudSync = attachCloudSync({
490
+ fs,
544
491
  reticleRoot,
545
- cloud: () => resolveProjectCloud(fs, reticleRoot, homedir(), process.env),
546
- // Every OTHER linked repo on this machine, not just the directory the daemon was started in.
547
- // One daemon serves many projects, and pushing only its own root left the rest silently
548
- // reporting nothing — indistinguishable, on the dashboard, from nobody having verified anything.
549
- otherRoots: () => Promise.resolve(knownProjectRoots()),
550
- cloudFor: (root) => resolveProjectCloud(fs, root, homedir(), process.env),
492
+ homeDir: homedir(),
493
+ env: process.env,
494
+ otherRoots: knownProjectRoots,
551
495
  });
552
496
  syncNudge.run = () => cloudSync.nudge();
553
- // The panel's sync button. `syncNow`, not `nudge`: a nudge schedules a cycle soon, which is right
554
- // for "a run landed" and wrong for a button somebody is watching. Never awaited.
497
+ // `syncNow`, not `nudge`: a nudge schedules a cycle soon, which is right for "a run landed" and
498
+ // wrong for a button somebody is watching. Never awaited.
555
499
  bridge.attachSyncRequest(() => void cloudSync.syncNow());
500
+ // The panel's harness switch. Written through to the platform rather than kept locally, so the
501
+ // console and the panel cannot disagree about a setting they both offer. Nothing is awaited and a
502
+ // failure is not surfaced: the next snapshot re-reads the platform, so a lost write shows up as
503
+ // the switch springing back, which is the truthful outcome.
504
+ bridge.attachHarnessRequest((enabled) => void writeHarnessSwitch(process.env, enabled));
556
505
  // Scope auto-selection to the active project (from .reticle.json) so a stray tab from another app is
557
506
  // never picked when the agent omits a sessionId. Explicit per-call scope/sessionId still overrides.
558
507
  // Scope + the no-session diagnosis: "no browser session connected" is the error that ends most
@@ -574,6 +523,7 @@ export async function startDaemon(options = {}) {
574
523
  project,
575
524
  fs,
576
525
  reticleRoot,
526
+ linkedCloud: linkedCloudPort(fs, reticleRoot, homedir(), process.env),
577
527
  // The long-lived daemon needs this MORE than the standalone MCP process does, not less: it is
578
528
  // the one that outlives a single project and serves every app on the machine. Omitting it here
579
529
  // silently disabled per-session artifact resolution for every agent that attaches to a running
@@ -591,7 +541,7 @@ export async function startDaemon(options = {}) {
591
541
  const effectiveDeps = realInput !== undefined ? { ...deps, realInput } : deps;
592
542
  // Read per attach, not once: a project that gets wired while this daemon is alive should stop
593
543
  // being told to wire itself on the next agent that connects.
594
- shared.attachMcp(() => createMcpServer(effectiveDeps, profile, hasProjectConnectedBefore(reticleStateHome(), port, readProjectId(process.cwd()))));
544
+ shared.attachMcp(() => createMcpServer(effectiveDeps, profile, hasAnyProjectConnectedBefore(reticleStateHome(), port, projectIdsAt(process.cwd()))));
595
545
  // `reticle drive <url>` when this daemon already owns the port: it asks HERE instead of trying to
596
546
  // bind a port we are holding, and gets the same pooled context an agent's reticle_lease returns —
597
547
  // through runTool, so it is counted and reported like any other call rather than being a second,
@@ -693,7 +643,7 @@ export async function startDaemon(options = {}) {
693
643
  if (vh !== undefined)
694
644
  await new Promise((resolve) => vh.server.close(() => resolve()));
695
645
  leaseReaper.stop();
696
- cloudSync.stop();
646
+ await cloudSync.flush(); // not stop(): the last run written is the one nobody has yet
697
647
  await loopbackAlias.close?.();
698
648
  await pool.shutdown();
699
649
  await owned?.dispose();
@@ -29,6 +29,18 @@ declare const CapsuleSchema: z.ZodObject<{
29
29
  observed: z.ZodString;
30
30
  /** Minimal steps to reproduce (prefix-trimmed). */
31
31
  steps: z.ZodArray<z.ZodType<FlowStep, z.ZodTypeDef, FlowStep>, "many">;
32
+ /**
33
+ * How many times this exact failure has been captured, and when it last was.
34
+ *
35
+ * A repeat is not a new capsule -- it is the same one, again. "This failed 37 times, most
36
+ * recently at T" is strictly MORE than 37 files each holding one timestamp, so folding the
37
+ * duplicates in adds information rather than discarding it.
38
+ *
39
+ * Optional, because a capsule written before these existed has neither and must not be read as
40
+ * though it were seen once when nobody counted.
41
+ */
42
+ seen: z.ZodOptional<z.ZodNumber>;
43
+ lastSeenAt: z.ZodOptional<z.ZodNumber>;
32
44
  }, "strip", z.ZodTypeAny, {
33
45
  expected: string;
34
46
  origin: string;
@@ -38,6 +50,8 @@ declare const CapsuleSchema: z.ZodObject<{
38
50
  createdAt: number;
39
51
  observed: string;
40
52
  flow?: string | undefined;
53
+ seen?: number | undefined;
54
+ lastSeenAt?: number | undefined;
41
55
  }, {
42
56
  expected: string;
43
57
  origin: string;
@@ -47,6 +61,8 @@ declare const CapsuleSchema: z.ZodObject<{
47
61
  createdAt: number;
48
62
  observed: string;
49
63
  flow?: string | undefined;
64
+ seen?: number | undefined;
65
+ lastSeenAt?: number | undefined;
50
66
  }>;
51
67
  export type Capsule = z.infer<typeof CapsuleSchema>;
52
68
  /** Capsule ids are filenames — refuse anything that could escape the capsules directory. */
@@ -63,8 +79,21 @@ export declare class CapsuleStore {
63
79
  /** Every readable capsule, newest-first — what `reticle capsules` prints. */
64
80
  all(): Promise<Capsule[]>;
65
81
  }
66
- /** Deterministic, path-safe capsule id: time-ordered so `list` sorts newest-first for free. */
67
- export declare function capsuleId(now: number, label: string): string;
82
+ /**
83
+ * What makes two captures the SAME failure.
84
+ *
85
+ * Everything that describes the failure, and nothing that describes the capture: `createdAt` is
86
+ * excluded because it is precisely what turned every retry of a broken step into a new file, and
87
+ * `id` because it is derived from this. `flow` is included -- the same assertion failing in two
88
+ * different journeys is two findings, not one.
89
+ */
90
+ export declare function capsuleFingerprint(capsule: Omit<Capsule, 'id' | 'createdAt' | 'seen' | 'lastSeenAt'>): string;
91
+ /**
92
+ * Deterministic, path-safe capsule id: time-ordered so `list` sorts newest-first for free, with the
93
+ * fingerprint on the END so a duplicate can be found by listing the directory rather than reading
94
+ * every file in it.
95
+ */
96
+ export declare function capsuleId(now: number, label: string, fingerprint: string): string;
68
97
  /** The minimal reproduction: the steps up to and including the one that failed (prefix trim). */
69
98
  export declare function minimalSteps(steps: readonly FlowStep[], failedIndex: number): FlowStep[];
70
99
  export {};
@@ -1,4 +1,6 @@
1
1
  import { join } from 'node:path';
2
+ import { fnv1a } from '@reticlehq/core';
3
+ import { writeFileAtomic } from '../../memory/project/fs/write-atomic.js';
2
4
  import { z } from 'zod';
3
5
  import { FlowStepSchema } from '@reticlehq/core';
4
6
  import { reticleDirPaths } from '../../memory/project/dir/reticle-dir.js';
@@ -30,6 +32,18 @@ const CapsuleSchema = z.object({
30
32
  observed: z.string(),
31
33
  /** Minimal steps to reproduce (prefix-trimmed). */
32
34
  steps: z.array(FlowStepSchema),
35
+ /**
36
+ * How many times this exact failure has been captured, and when it last was.
37
+ *
38
+ * A repeat is not a new capsule -- it is the same one, again. "This failed 37 times, most
39
+ * recently at T" is strictly MORE than 37 files each holding one timestamp, so folding the
40
+ * duplicates in adds information rather than discarding it.
41
+ *
42
+ * Optional, because a capsule written before these existed has neither and must not be read as
43
+ * though it were seen once when nobody counted.
44
+ */
45
+ seen: z.number().int().positive().optional(),
46
+ lastSeenAt: z.number().optional(),
33
47
  });
34
48
  /** Capsule ids are filenames — refuse anything that could escape the capsules directory. */
35
49
  export function isValidCapsuleId(id) {
@@ -51,9 +65,26 @@ export class CapsuleStore {
51
65
  return false;
52
66
  try {
53
67
  await this.#fs.mkdir(this.#dir);
54
- const tmp = `${this.#pathFor(capsule.id)}.tmp`;
55
- await this.#fs.writeFile(tmp, `${JSON.stringify(capsule, null, 2)}\n`);
56
- await this.#fs.rename(tmp, this.#pathFor(capsule.id));
68
+ /*
69
+ * A capture of a failure already on disk folds into it rather than adding a file. Found by
70
+ * LISTING: the fingerprint is the id's suffix, so this costs one directory read instead of
71
+ * one read per capsule. An id written before fingerprints existed simply never matches, which
72
+ * is the correct outcome -- it is left exactly as it was.
73
+ *
74
+ * `createdAt` keeps the FIRST sighting. A repeat is the same failure happening again, not a
75
+ * new one, and the interesting pair is when it started and when it last happened.
76
+ */
77
+ const fingerprint = capsuleFingerprint(capsule);
78
+ const existingId = (await this.list()).find((id) => id.endsWith(`-${fingerprint}`));
79
+ const previous = existingId === undefined ? undefined : await this.read(existingId);
80
+ const folded = previous === undefined
81
+ ? { ...capsule, seen: 1, lastSeenAt: capsule.createdAt }
82
+ : {
83
+ ...previous,
84
+ seen: (previous.seen ?? 1) + 1,
85
+ lastSeenAt: capsule.createdAt,
86
+ };
87
+ await writeFileAtomic(this.#fs, this.#pathFor(folded.id), `${JSON.stringify(folded, null, 2)}\n`);
57
88
  return true;
58
89
  }
59
90
  catch {
@@ -100,10 +131,31 @@ export class CapsuleStore {
100
131
  return out;
101
132
  }
102
133
  }
103
- /** Deterministic, path-safe capsule id: time-ordered so `list` sorts newest-first for free. */
104
- export function capsuleId(now, label) {
134
+ /**
135
+ * What makes two captures the SAME failure.
136
+ *
137
+ * Everything that describes the failure, and nothing that describes the capture: `createdAt` is
138
+ * excluded because it is precisely what turned every retry of a broken step into a new file, and
139
+ * `id` because it is derived from this. `flow` is included -- the same assertion failing in two
140
+ * different journeys is two findings, not one.
141
+ */
142
+ export function capsuleFingerprint(capsule) {
143
+ return fnv1a(JSON.stringify([
144
+ capsule.flow ?? '',
145
+ capsule.origin,
146
+ capsule.expected,
147
+ capsule.observed,
148
+ capsule.steps,
149
+ ]));
150
+ }
151
+ /**
152
+ * Deterministic, path-safe capsule id: time-ordered so `list` sorts newest-first for free, with the
153
+ * fingerprint on the END so a duplicate can be found by listing the directory rather than reading
154
+ * every file in it.
155
+ */
156
+ export function capsuleId(now, label, fingerprint) {
105
157
  const safe = label.replace(/[^a-zA-Z0-9._-]/g, '-').slice(0, 40);
106
- return `${String(now)}-${safe.length > 0 ? safe : 'capsule'}`;
158
+ return `${String(now)}-${safe.length > 0 ? safe : 'capsule'}-${fingerprint}`;
107
159
  }
108
160
  /** The minimal reproduction: the steps up to and including the one that failed (prefix trim). */
109
161
  export function minimalSteps(steps, failedIndex) {
@@ -1,4 +1,5 @@
1
1
  import { ConsequenceKind, EventType } from '@reticlehq/core';
2
+ import { isDocumentInitiated } from '@reticlehq/engine/question/predicate/predicate-eval.js';
2
3
  import { causalSummary } from './causal-summary.js';
3
4
  import { firstDivergence } from './divergence.js';
4
5
  /**
@@ -34,6 +35,10 @@ export function blastRadius(expected, observed) {
34
35
  continue;
35
36
  if (declaredUrls.some((fragment) => url.includes(fragment)))
36
37
  continue;
38
+ // A subresource the document fetched is the page loading, not a request the action sent: one
39
+ // navigation is a couple of hundred of them. IPC and beacons carry an initiator too, and stay.
40
+ if (isDocumentInitiated(event))
41
+ continue;
37
42
  const method = event.data['method'];
38
43
  add(`net ${'string' === typeof method ? method : 'request'} ${url}`);
39
44
  continue;
@@ -63,6 +63,18 @@ export interface CausalSummary {
63
63
  elided?: Partial<Record<CappedList, number>>;
64
64
  route?: string;
65
65
  signals: string[];
66
+ /**
67
+ * Form fields whose value moved in this window, by NAME.
68
+ *
69
+ * Names only, never values: a form is where somebody types their password, and the observer that
70
+ * reports this already refuses to carry one. The names are what an assertion is written against,
71
+ * and a field moving when nothing should have touched it (or not moving when something should)
72
+ * is the silently-cleared-field class — which until this channel existed left no trace at all,
73
+ * because React sets the input PROPERTY and MutationObserver watches attributes.
74
+ *
75
+ * Present only when a field moved, so a page with no form pays nothing.
76
+ */
77
+ fieldsChanged?: string[];
66
78
  layoutShift?: number;
67
79
  longTasks: number;
68
80
  /**
@@ -114,6 +126,6 @@ interface SummaryContext {
114
126
  */
115
127
  export declare const MAX_SUMMARY_ENTRIES = 20;
116
128
  /** The lists that are capped, named so `elided` can say which one lost entries. */
117
- type CappedList = 'statePathsChanged' | 'storageKeysChanged' | 'stateDiffs' | 'storageDiffs' | 'signals';
129
+ type CappedList = 'statePathsChanged' | 'storageKeysChanged' | 'stateDiffs' | 'storageDiffs' | 'signals' | 'fieldsChanged';
118
130
  export declare function causalSummary(events: readonly ReticleEvent[], context?: SummaryContext): CausalSummary;
119
131
  export {};
@@ -69,6 +69,7 @@ export function causalSummary(events, context = {}) {
69
69
  const storageDiffs = [];
70
70
  let route;
71
71
  const signals = [];
72
+ const fieldsChanged = [];
72
73
  let layoutShift;
73
74
  let longTasks = 0;
74
75
  for (const event of events) {
@@ -131,6 +132,9 @@ export function causalSummary(events, context = {}) {
131
132
  case EventType.SIGNAL:
132
133
  pushUnique(signals, data['name']);
133
134
  break;
135
+ case EventType.FIELD_CHANGE:
136
+ pushUnique(fieldsChanged, data['field']);
137
+ break;
134
138
  case EventType.PERF:
135
139
  if (data['metric'] === PerfMetric.CLS && 'number' === typeof data['value']) {
136
140
  layoutShift = Math.max(layoutShift ?? 0, data['value']);
@@ -163,6 +167,9 @@ export function causalSummary(events, context = {}) {
163
167
  storageDiffs: cap(storageDiffs, 'storageDiffs', elided),
164
168
  ...(route === undefined ? {} : { route }),
165
169
  signals: cap(signals, 'signals', elided),
170
+ ...(0 === fieldsChanged.length
171
+ ? {}
172
+ : { fieldsChanged: cap(fieldsChanged, 'fieldsChanged', elided) }),
166
173
  ...(0 === Object.keys(elided).length ? {} : { elided }),
167
174
  ...(layoutShift === undefined ? {} : { layoutShift }),
168
175
  longTasks,