@reticlehq/server 3.4.0 → 3.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 (350) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +16 -8
  3. package/dist/chromium-launch-options.d.ts +3 -1
  4. package/dist/chromium-launch-options.js +2 -1
  5. package/dist/command/cli/adhoc-verdict.d.ts +1 -0
  6. package/dist/command/cli/adhoc-verdict.js +6 -3
  7. package/dist/command/cli/answers/open-note.d.ts +2 -0
  8. package/dist/command/cli/answers/open-note.js +22 -0
  9. package/dist/command/cli/auth/cloud-session.d.ts +4 -4
  10. package/dist/command/cli/auth/device-flow.d.ts +2 -2
  11. package/dist/command/cli/cli-flow-commands.js +4 -8
  12. package/dist/command/cli/cli-parse.d.ts +1 -1
  13. package/dist/command/cli/cli-parse.js +13 -11
  14. package/dist/command/cli/cli-verify.d.ts +1 -1
  15. package/dist/command/cli/cli-verify.js +35 -25
  16. package/dist/command/cli/cloud-cli.js +110 -12
  17. package/dist/command/cli/cloud-login.d.ts +9 -1
  18. package/dist/command/cli/cloud-login.js +43 -8
  19. package/dist/command/cli/demo/demo-page.js +11 -3
  20. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +46 -0
  21. package/dist/command/cli/doctor/browser/chromium-hint.js +99 -1
  22. package/dist/command/cli/drive/drive-attach.d.ts +27 -2
  23. package/dist/command/cli/drive/drive-attach.js +71 -4
  24. package/dist/command/cli/drive/drive-command.d.ts +2 -1
  25. package/dist/command/cli/drive/drive-command.js +13 -7
  26. package/dist/command/cli/lifecycle/daemon-lifecycle.js +4 -1
  27. package/dist/command/cli/ports/port-holder.d.ts +10 -0
  28. package/dist/command/cli/ports/port-holder.js +24 -0
  29. package/dist/command/cli/setup-mcp-cli.js +15 -5
  30. package/dist/command/cli/tutorial.d.ts +2 -2
  31. package/dist/command/cli/tutorial.js +6 -9
  32. package/dist/command/cli.d.ts +6 -0
  33. package/dist/command/cli.js +45 -23
  34. package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
  35. package/dist/command/daemon/binding/daemon-status-probe.js +1 -0
  36. package/dist/command/daemon/daemon-resolve.d.ts +14 -0
  37. package/dist/command/daemon/daemon-resolve.js +33 -2
  38. package/dist/command/setup/agent-writer.d.ts +1 -1
  39. package/dist/command/setup/agent-writer.js +3 -2
  40. package/dist/command/setup/bringup/connect-lease.d.ts +1 -0
  41. package/dist/command/setup/bringup/connect-lease.js +1 -0
  42. package/dist/command/setup/bringup/dev-server-wait.d.ts +10 -0
  43. package/dist/command/setup/bringup/dev-server-wait.js +20 -1
  44. package/dist/command/setup/init/init-port.d.ts +4 -0
  45. package/dist/command/setup/init/init-port.js +10 -0
  46. package/dist/command/setup/init/init-runtime.d.ts +10 -0
  47. package/dist/command/setup/init/init-runtime.js +71 -9
  48. package/dist/command/setup/node-effects.js +8 -2
  49. package/dist/command/setup/remaining-steps.js +2 -1
  50. package/dist/command/setup/run-setup.d.ts +7 -0
  51. package/dist/command/setup/run-setup.js +38 -25
  52. package/dist/command/setup/setup-command.d.ts +2 -1
  53. package/dist/command/setup/setup-command.js +2 -0
  54. package/dist/command/setup/setup-mcp.d.ts +2 -0
  55. package/dist/command/setup/setup-mcp.js +14 -5
  56. package/dist/command/version/peer-announce.d.ts +6 -2
  57. package/dist/command/version/peer-announce.js +8 -6
  58. package/dist/command/version/version-nudge.d.ts +17 -3
  59. package/dist/command/version/version-nudge.js +25 -4
  60. package/dist/command/version/version-skew.js +11 -2
  61. package/dist/features/crawl/crawl-tools.js +4 -0
  62. package/dist/features/crawl/crawl.d.ts +6 -0
  63. package/dist/features/crawl/crawl.js +9 -1
  64. package/dist/features/exhaust/ledger.d.ts +7 -0
  65. package/dist/features/exhaust/ledger.js +35 -9
  66. package/dist/features/harness/app-findings.d.ts +2 -0
  67. package/dist/features/harness/app-findings.js +52 -0
  68. package/dist/features/harness/drive-report.d.ts +27 -1
  69. package/dist/features/harness/drive-report.js +162 -11
  70. package/dist/features/harness/drivers.d.ts +10 -14
  71. package/dist/features/harness/drivers.js +10 -14
  72. package/dist/features/harness/fill-values.d.ts +3 -69
  73. package/dist/features/harness/fill-values.js +3 -133
  74. package/dist/features/harness/goals.d.ts +21 -0
  75. package/dist/features/harness/goals.js +77 -0
  76. package/dist/features/harness/harness.d.ts +21 -0
  77. package/dist/features/harness/harness.js +69 -2
  78. package/dist/features/harness/platform/drive-target.d.ts +30 -0
  79. package/dist/features/harness/platform/drive-target.js +81 -0
  80. package/dist/features/harness/platform/local-apps.d.ts +94 -0
  81. package/dist/features/harness/platform/local-apps.js +204 -0
  82. package/dist/features/harness/platform/remote-drive.d.ts +180 -0
  83. package/dist/features/harness/platform/remote-drive.js +365 -0
  84. package/dist/features/harness/platform/script.d.ts +48 -0
  85. package/dist/features/harness/platform/script.js +128 -0
  86. package/dist/features/harness/platform/server-driver.d.ts +41 -0
  87. package/dist/features/harness/platform/server-driver.js +179 -0
  88. package/dist/features/harness/platform-config.d.ts +7 -0
  89. package/dist/features/harness/platform-config.js +14 -2
  90. package/dist/features/harness/script/app-controls.d.ts +5 -0
  91. package/dist/features/harness/script/app-controls.js +40 -0
  92. package/dist/features/harness/script/lane-ids.d.ts +14 -0
  93. package/dist/features/harness/script/lane-ids.js +16 -0
  94. package/dist/features/harness/script/script-run.d.ts +41 -0
  95. package/dist/features/harness/script/script-run.js +299 -0
  96. package/dist/features/license/license.d.ts +2 -2
  97. package/dist/features/visual/visual-tools.d.ts +10 -1
  98. package/dist/features/visual/visual-tools.js +40 -3
  99. package/dist/hooks/driven-by.d.ts +4 -0
  100. package/dist/hooks/driven-by.js +16 -0
  101. package/dist/hooks/hook-commands.js +15 -4
  102. package/dist/hooks/tool-log.d.ts +9 -0
  103. package/dist/hooks/tool-log.js +29 -0
  104. package/dist/index.d.ts +1 -0
  105. package/dist/index.js +91 -89
  106. package/dist/judgement/capsule/capsule-store.d.ts +4 -4
  107. package/dist/judgement/runs/artifact/build-verification-run.js +6 -0
  108. package/dist/judgement/runs/artifact/replay-mapping.d.ts +1 -1
  109. package/dist/judgement/runs/artifact/replay-mapping.js +16 -6
  110. package/dist/judgement/runs/artifact/run-store.d.ts +1 -1
  111. package/dist/judgement/runs/artifact/run-store.js +26 -1
  112. package/dist/judgement/runs/drive-run.d.ts +22 -0
  113. package/dist/judgement/runs/drive-run.js +99 -1
  114. package/dist/judgement/runs/repair-prompt.js +1 -1
  115. package/dist/judgement/runs/reticle-runner.js +8 -1
  116. package/dist/judgement/runs/verification-sync.d.ts +3 -2
  117. package/dist/judgement/runs/verification-sync.js +4 -3
  118. package/dist/language/flows/change/gate.d.ts +10 -0
  119. package/dist/language/flows/change/gate.js +22 -0
  120. package/dist/language/flows/decision.js +34 -0
  121. package/dist/language/flows/flow-anchor.d.ts +54 -8
  122. package/dist/language/flows/flow-anchor.js +88 -3
  123. package/dist/language/flows/flow-cross-step.d.ts +15 -0
  124. package/dist/language/flows/flow-cross-step.js +21 -0
  125. package/dist/language/flows/flow-learning.js +8 -11
  126. package/dist/language/flows/flow-mutate-tools.js +5 -3
  127. package/dist/language/flows/flow-replay-run.d.ts +23 -9
  128. package/dist/language/flows/flow-replay-run.js +101 -94
  129. package/dist/language/flows/flow-replay.d.ts +23 -1
  130. package/dist/language/flows/flow-replay.js +92 -19
  131. package/dist/language/flows/flow-scope.js +5 -1
  132. package/dist/language/flows/flow-step-runners.d.ts +20 -2
  133. package/dist/language/flows/flow-step-runners.js +236 -42
  134. package/dist/language/flows/flow-store-for-session.d.ts +2 -2
  135. package/dist/language/flows/flow-store-for-session.js +2 -2
  136. package/dist/language/flows/flow-tools.js +58 -34
  137. package/dist/language/flows/flows.js +5 -2
  138. package/dist/language/flows/heal-run.js +6 -4
  139. package/dist/language/flows/recording/flake.d.ts +2 -2
  140. package/dist/language/flows/recording/tape/recordings.d.ts +30 -4
  141. package/dist/language/flows/recording/tape/recordings.js +40 -8
  142. package/dist/language/flows/replay-from-hud.d.ts +20 -0
  143. package/dist/language/flows/replay-from-hud.js +51 -0
  144. package/dist/language/flows/replay.d.ts +9 -5
  145. package/dist/language/flows/replay.js +27 -9
  146. package/dist/language/flows/suite/suite-flakes.js +2 -0
  147. package/dist/language/flows/suite-selection.d.ts +2 -15
  148. package/dist/language/flows/suite-selection.js +4 -3
  149. package/dist/language/flows/verify-change-tools.js +8 -4
  150. package/dist/launch-chromium.d.ts +27 -5
  151. package/dist/launch-chromium.js +38 -8
  152. package/dist/memory/cloud/cloud-config.d.ts +12 -0
  153. package/dist/memory/cloud/cloud-config.js +15 -0
  154. package/dist/memory/cloud/cloud-sync.d.ts +17 -14
  155. package/dist/memory/cloud/cloud-sync.js +12 -1
  156. package/dist/memory/cloud/harness-config.d.ts +26 -1
  157. package/dist/memory/cloud/harness-config.js +99 -4
  158. package/dist/memory/cloud/hud-notices-source.d.ts +21 -0
  159. package/dist/memory/cloud/hud-notices-source.js +128 -0
  160. package/dist/memory/cloud/memory-scope.d.ts +144 -0
  161. package/dist/memory/cloud/memory-scope.js +205 -0
  162. package/dist/memory/cloud/sync-cycle.d.ts +21 -3
  163. package/dist/memory/cloud/sync-cycle.js +45 -5
  164. package/dist/memory/cloud/sync-daemon.js +7 -5
  165. package/dist/memory/cloud/sync-disk.d.ts +1 -1
  166. package/dist/memory/cloud/sync-disk.js +5 -8
  167. package/dist/memory/impact/account-watch.d.ts +19 -0
  168. package/dist/memory/impact/account-watch.js +34 -0
  169. package/dist/memory/impact/impact-recorder.d.ts +8 -0
  170. package/dist/memory/impact/impact-recorder.js +12 -2
  171. package/dist/memory/impact/impact-store.d.ts +16 -0
  172. package/dist/memory/impact/impact-store.js +142 -26
  173. package/dist/memory/intent/intent-shard.d.ts +2 -2
  174. package/dist/memory/intent/intent-tools.js +88 -5
  175. package/dist/memory/journal/envelope.d.ts +16 -16
  176. package/dist/memory/journal/journal-query.d.ts +14 -0
  177. package/dist/memory/journal/journal-query.js +15 -0
  178. package/dist/memory/journal/journal-recorder.js +7 -1
  179. package/dist/memory/journal/on-disk/retention.d.ts +3 -0
  180. package/dist/memory/journal/on-disk/retention.js +5 -0
  181. package/dist/memory/journal/on-disk/startup-maintenance.js +3 -1
  182. package/dist/memory/journal/on-disk/workspace-tiers.js +7 -4
  183. package/dist/memory/journal/session-end.js +6 -4
  184. package/dist/memory/project/artifact-root-resolver.d.ts +36 -1
  185. package/dist/memory/project/artifact-root-resolver.js +134 -26
  186. package/dist/memory/project/artifact-root.d.ts +42 -5
  187. package/dist/memory/project/artifact-root.js +87 -8
  188. package/dist/memory/project/dir/reticle-dir.d.ts +4 -2
  189. package/dist/memory/project/dir/reticle-dir.js +3 -1
  190. package/dist/memory/project/project-tools.js +22 -3
  191. package/dist/memory/project/session-root.d.ts +24 -1
  192. package/dist/memory/project/session-root.js +41 -5
  193. package/dist/memory/project/sync-status.d.ts +39 -0
  194. package/dist/memory/project/sync-status.js +90 -0
  195. package/dist/memory/recall/memory-tools.js +2 -0
  196. package/dist/memory/recall/project-memory.d.ts +19 -0
  197. package/dist/memory/recall/project-memory.js +45 -6
  198. package/dist/portal/bridge/bridge.d.ts +7 -4
  199. package/dist/portal/bridge/bridge.js +25 -3
  200. package/dist/portal/bridge/pairing-token.d.ts +9 -0
  201. package/dist/portal/bridge/pairing-token.js +17 -0
  202. package/dist/portal/input/real-input.d.ts +81 -10
  203. package/dist/portal/input/real-input.js +207 -22
  204. package/dist/portal/input/viewport-tools.js +19 -9
  205. package/dist/portal/pool/browser-pool.d.ts +22 -9
  206. package/dist/portal/pool/browser-pool.js +70 -35
  207. package/dist/portal/pool/playwright-launcher.d.ts +13 -0
  208. package/dist/portal/pool/playwright-launcher.js +17 -1
  209. package/dist/portal/pool/pool-contract.d.ts +7 -4
  210. package/dist/portal/session/command-payload.d.ts +8 -0
  211. package/dist/portal/session/command-payload.js +19 -0
  212. package/dist/portal/session/dev-server/dev-command.d.ts +12 -0
  213. package/dist/portal/session/dev-server/dev-command.js +61 -12
  214. package/dist/portal/session/facts/departure-note.d.ts +22 -0
  215. package/dist/portal/session/facts/departure-note.js +47 -0
  216. package/dist/portal/session/facts/session-replaced.d.ts +5 -0
  217. package/dist/portal/session/facts/session-replaced.js +5 -0
  218. package/dist/portal/session/first-run-wiring.d.ts +42 -0
  219. package/dist/portal/session/first-run-wiring.js +132 -0
  220. package/dist/portal/session/human/notes-ledger.d.ts +20 -0
  221. package/dist/portal/session/human/notes-ledger.js +55 -0
  222. package/dist/portal/session/human/review-store.d.ts +12 -0
  223. package/dist/portal/session/human/review-store.js +18 -10
  224. package/dist/portal/session/live-control-tools.d.ts +0 -13
  225. package/dist/portal/session/live-control-tools.js +28 -3
  226. package/dist/portal/session/no-session-diagnosis.d.ts +7 -0
  227. package/dist/portal/session/no-session-diagnosis.js +15 -0
  228. package/dist/portal/session/no-session-next-action.d.ts +11 -0
  229. package/dist/portal/session/no-session-next-action.js +31 -5
  230. package/dist/portal/session/no-session-watch.d.ts +14 -1
  231. package/dist/portal/session/no-session-watch.js +63 -10
  232. package/dist/portal/session/presence/session-mirror.js +3 -0
  233. package/dist/portal/session/serving-directory.d.ts +27 -0
  234. package/dist/portal/session/serving-directory.js +45 -0
  235. package/dist/portal/session/session-health.d.ts +7 -0
  236. package/dist/portal/session/session-health.js +6 -0
  237. package/dist/portal/session/session-info.d.ts +8 -1
  238. package/dist/portal/session/session-info.js +6 -0
  239. package/dist/portal/session/session-manager.d.ts +21 -0
  240. package/dist/portal/session/session-manager.js +44 -8
  241. package/dist/portal/session/session-reaper.js +4 -1
  242. package/dist/portal/session/session-successor.d.ts +5 -0
  243. package/dist/portal/session/session-tools.js +26 -3
  244. package/dist/portal/session/session.d.ts +12 -10
  245. package/dist/portal/session/session.js +30 -31
  246. package/dist/start-options.d.ts +6 -0
  247. package/dist/status-payload.d.ts +10 -0
  248. package/dist/status-payload.js +23 -0
  249. package/dist/surface/http-server.d.ts +1 -1
  250. package/dist/surface/http-server.js +15 -4
  251. package/dist/surface/mcp/mcp-proxy.d.ts +5 -0
  252. package/dist/surface/mcp/mcp-proxy.js +36 -5
  253. package/dist/surface/mcp/mcp.d.ts +11 -6
  254. package/dist/surface/mcp/mcp.js +28 -14
  255. package/dist/surface/mcp/proxy/proxy-log.js +3 -5
  256. package/dist/surface/mcp/server-instructions.d.ts +4 -2
  257. package/dist/surface/mcp/server-instructions.js +14 -4
  258. package/dist/surface/mcp/shared-params.js +2 -2
  259. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +8 -0
  260. package/dist/surface/tools/act/navigation/navigate-arrival.js +56 -2
  261. package/dist/surface/tools/act/settle-in-flight.js +25 -3
  262. package/dist/surface/tools/act-sequence-tool.js +2 -2
  263. package/dist/surface/tools/act-tools.js +25 -9
  264. package/dist/surface/tools/assert/assert-verdict.d.ts +1 -1
  265. package/dist/surface/tools/assert/assert-verdict.js +17 -2
  266. package/dist/surface/tools/browser-tools.js +8 -1
  267. package/dist/surface/tools/chat-drives.d.ts +26 -0
  268. package/dist/surface/tools/chat-drives.js +165 -0
  269. package/dist/surface/tools/dynamic-tools.js +29 -2
  270. package/dist/surface/tools/empty-session-list.d.ts +41 -0
  271. package/dist/surface/tools/empty-session-list.js +80 -0
  272. package/dist/surface/tools/error-recovery.d.ts +6 -0
  273. package/dist/surface/tools/error-recovery.js +38 -6
  274. package/dist/surface/tools/explore-tools.d.ts +10 -1
  275. package/dist/surface/tools/explore-tools.js +82 -13
  276. package/dist/surface/tools/feedback-tools.js +14 -0
  277. package/dist/surface/tools/harness-explore.d.ts +96 -14
  278. package/dist/surface/tools/harness-explore.js +227 -202
  279. package/dist/surface/tools/harness-plan.d.ts +12 -0
  280. package/dist/surface/tools/harness-plan.js +31 -0
  281. package/dist/surface/tools/harness-script.d.ts +8 -0
  282. package/dist/surface/tools/harness-script.js +342 -0
  283. package/dist/surface/tools/harness-toolset.d.ts +14 -1
  284. package/dist/surface/tools/harness-toolset.js +77 -6
  285. package/dist/surface/tools/invoke-tool.js +64 -9
  286. package/dist/surface/tools/lease-seed.d.ts +29 -0
  287. package/dist/surface/tools/lease-seed.js +123 -0
  288. package/dist/surface/tools/lease-tools.d.ts +3 -23
  289. package/dist/surface/tools/lease-tools.js +30 -131
  290. package/dist/surface/tools/live-call-text.js +2 -2
  291. package/dist/surface/tools/next-step.d.ts +19 -0
  292. package/dist/surface/tools/next-step.js +159 -0
  293. package/dist/surface/tools/observe-tools.js +2 -4
  294. package/dist/surface/tools/platform-moment.d.ts +12 -0
  295. package/dist/surface/tools/platform-moment.js +168 -0
  296. package/dist/surface/tools/query-hint-schema.d.ts +7 -0
  297. package/dist/surface/tools/query-hint-schema.js +10 -0
  298. package/dist/surface/tools/read-tools.js +8 -2
  299. package/dist/surface/tools/real-input-attempt.js +96 -13
  300. package/dist/surface/tools/tool-kit.d.ts +25 -1
  301. package/dist/surface/tools/tool-kit.js +4 -6
  302. package/dist/surface/tools/tools.js +20 -48
  303. package/dist/telemetry/argument-shape.js +1 -1
  304. package/dist/telemetry/hud-metrics.d.ts +2 -0
  305. package/dist/telemetry/hud-metrics.js +17 -0
  306. package/dist/telemetry/onboarding-firsts.d.ts +2 -2
  307. package/dist/telemetry/onboarding-firsts.js +2 -2
  308. package/dist/telemetry/telemetry.d.ts +12 -0
  309. package/dist/telemetry/telemetry.js +43 -14
  310. package/dist/telemetry/verification-of.js +1 -0
  311. package/dist/wire-journal.js +4 -1
  312. package/docs/README.md +1 -1
  313. package/docs/actions.mdx +1 -2
  314. package/docs/agent-cheatsheet.md +1 -1
  315. package/docs/autodrive.md +29 -25
  316. package/docs/cli/cloud.mdx +6 -4
  317. package/docs/cli/doctor.mdx +6 -0
  318. package/docs/cli/drive.mdx +8 -2
  319. package/docs/cli/init.mdx +11 -13
  320. package/docs/cli/status.mdx +5 -1
  321. package/docs/cli/version.mdx +3 -3
  322. package/docs/cli.mdx +14 -8
  323. package/docs/desktop.mdx +5 -1
  324. package/docs/environment.mdx +3 -7
  325. package/docs/getting-started.md +14 -2
  326. package/docs/human-control.md +1 -2
  327. package/docs/index.mdx +4 -3
  328. package/docs/install-agentic.mdx +1 -1
  329. package/docs/install-manual.mdx +2 -2
  330. package/docs/packages/next.mdx +3 -0
  331. package/docs/packages/tauri.mdx +2 -2
  332. package/docs/packages/vite-plugin.mdx +24 -1
  333. package/docs/packages.mdx +1 -1
  334. package/docs/quickstart.mdx +23 -11
  335. package/docs/telemetry.md +1 -1
  336. package/docs/tools/act.mdx +1 -1
  337. package/docs/tools/session-and-feedback.mdx +9 -9
  338. package/docs/usage.md +5 -3
  339. package/docs/what-is-recorded.md +13 -3
  340. package/docs/what-is-reticle.mdx +3 -1
  341. package/package.json +8 -12
  342. package/dist/features/harness/driver.d.ts +0 -50
  343. package/dist/features/harness/driver.js +0 -236
  344. package/dist/features/harness/jev-driver.d.ts +0 -188
  345. package/dist/features/harness/jev-driver.js +0 -678
  346. package/dist/features/harness/openai-driver.d.ts +0 -46
  347. package/dist/features/harness/openai-driver.js +0 -194
  348. package/dist/memory/project/dir/fill-value-store.d.ts +0 -32
  349. package/dist/memory/project/dir/fill-value-store.js +0 -88
  350. package/docs/local-registry.md +0 -143
@@ -0,0 +1,168 @@
1
+ /**
2
+ * What the agent is told about the Reticle platform, and when. One channel, two audiences.
3
+ *
4
+ * A machine that is NOT linked hears about the platform at real moments only, each once per project
5
+ * and never as an ad: verified runs that exist only here, a flow whose failure history nobody else
6
+ * can see, and a `.reticle` that has grown large. Each is a fact about the user's own work, with the
7
+ * one command that changes it.
8
+ *
9
+ * A machine that IS linked hears where its work stands: once when the platform first confirms it,
10
+ * then only when that changes (the platform refused a run, a push failed, or it recovered). Silence
11
+ * means it is going up. The daemon pushes after every run; an agent can force one with
12
+ * `reticle_project { push }`.
13
+ *
14
+ * Remembered in `.reticle`, so a restarted daemon does not repeat itself. Never throws, never blocks
15
+ * a verdict, and an embedder with no link port hears nothing.
16
+ */
17
+ import { readdirSync, statSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+ import { ReticleDir, Verified } from '@reticlehq/core';
20
+ import { reticleDirPaths } from '../../memory/project/dir/reticle-dir.js';
21
+ import { FlakeFileSchema } from '../../language/flows/recording/flake.js';
22
+ import { SyncStatus, describeSync, overallStatus, readSyncSummary, } from '../../memory/project/sync-status.js';
23
+ /** Verified runs kept only on this machine before the platform is worth a sentence. */
24
+ export const LOCAL_RUNS_MOMENT = 5;
25
+ /** A `.reticle` this large is history worth keeping somewhere other than one laptop. */
26
+ export const LARGE_RETICLE_BYTES = 50 * 1024 * 1024;
27
+ /** A flow that has failed this often has a history worth sharing. */
28
+ const FLAKY_MIN_RUNS = 3;
29
+ /** How often the linked status is re-read: it reads every run, so not on every verdict. */
30
+ const STATUS_EVERY_MS = 10_000;
31
+ /** How many verdicts between size checks: walking `.reticle` is not free either. */
32
+ const SIZE_EVERY = 20;
33
+ const MOMENTS_FILE = ReticleDir.PLATFORM_MOMENTS_FILE;
34
+ const CONNECT = '`npx @reticlehq/server connect`';
35
+ const RUN_FILE = /\.json$/;
36
+ const Moment = {
37
+ LOCAL_RUNS: 'local-runs',
38
+ FLAKE: 'flake-history',
39
+ LARGE: 'large-reticle',
40
+ };
41
+ /** Per root, in this process: what is known, so the file is read once and checks are throttled. */
42
+ const seen = new Map();
43
+ export async function takePlatformMoment(deps, raw,
44
+ /** The `.reticle` of the project the verdict was about — the session's, not the daemon's. */
45
+ rootOf) {
46
+ if (!('verified' in raw) || deps.linkedCloud === undefined)
47
+ return undefined;
48
+ try {
49
+ const root = rootOf();
50
+ const now = (deps.now ?? Date.now)();
51
+ const entry = seen.get(root) ?? { told: await readTold(deps, root), statusAt: 0, verdicts: 0 };
52
+ seen.set(root, entry);
53
+ entry.verdicts += 1;
54
+ const linked = (await deps.linkedCloud()) !== null;
55
+ const note = linked
56
+ ? linkedNote(root, entry, now)
57
+ : Verified.YES === raw['verified']
58
+ ? await unlinkedNote(deps, root, entry)
59
+ : undefined;
60
+ if (note !== undefined)
61
+ await writeTold(deps, root, entry.told);
62
+ return note;
63
+ }
64
+ catch {
65
+ return undefined; // a note never costs a tool call anything
66
+ }
67
+ }
68
+ /** Where a linked project's work stands, said only when that changed. */
69
+ function linkedNote(root, entry, now) {
70
+ if (0 !== entry.statusAt && now - entry.statusAt < STATUS_EVERY_MS)
71
+ return undefined;
72
+ entry.statusAt = now;
73
+ const summary = readSyncSummary(root, true);
74
+ if (0 === summary.runs)
75
+ return undefined;
76
+ const status = overallStatus(summary);
77
+ // The key is what would change the sentence's meaning, not its numbers: a count going up is the
78
+ // sync working, and saying so on every verdict would be noise.
79
+ const key = [status, ...summary.refused.map((r) => r.runId), summary.lastError ?? ''].join('|');
80
+ if (key === entry.told.status)
81
+ return undefined;
82
+ const first = entry.told.status === undefined;
83
+ const wasTrouble = !first && !(entry.told.status ?? '').startsWith(SyncStatus.ON_PLATFORM);
84
+ entry.told.status = key;
85
+ // A backlog the next push will clear is not news unless something went wrong with it.
86
+ if (status === SyncStatus.PENDING && summary.lastError === undefined)
87
+ return undefined;
88
+ if (status === SyncStatus.ON_PLATFORM && !first && !wasTrouble)
89
+ return undefined;
90
+ const lead = status === SyncStatus.ON_PLATFORM
91
+ ? first
92
+ ? 'Verified runs reach the Reticle platform automatically after each verdict: '
93
+ : 'Sync to the Reticle platform recovered: '
94
+ : 'Sync to the Reticle platform needs attention: ';
95
+ return (`${lead}${describeSync(summary, now)} The daemon pushes on its own; there is nothing for you ` +
96
+ 'to run. Tell the user in one line only if this needs them.');
97
+ }
98
+ /** The first real moment this project has reached and not yet been told about. */
99
+ async function unlinkedNote(deps, root, entry) {
100
+ const told = new Set(entry.told.moments);
101
+ const say = (moment, text) => {
102
+ entry.told.moments = [...told, moment];
103
+ return `${text} ${CONNECT} keeps it on the Reticle platform. Mention it to the user once, in one line, after your answer; do not repeat it.`;
104
+ };
105
+ if (!told.has(Moment.LOCAL_RUNS)) {
106
+ const runs = (await deps.fs.readdir(reticleDirPaths(root).runs)).filter((f) => RUN_FILE.test(f));
107
+ if (runs.length >= LOCAL_RUNS_MOMENT)
108
+ return say(Moment.LOCAL_RUNS, `${String(runs.length)} verified runs of this project exist only on this machine: a teammate cannot replay them and no dashboard tracks them.`);
109
+ }
110
+ if (!told.has(Moment.FLAKE)) {
111
+ const flaky = await flakyFlow(deps, root);
112
+ if (flaky !== undefined)
113
+ return say(Moment.FLAKE, `The flow "${flaky.name}" has failed ${String(flaky.fails)} of ${String(flaky.runs)} runs here, and that history lives only on this machine, so nobody else can see it is unreliable.`);
114
+ }
115
+ if (!told.has(Moment.LARGE) && 0 === entry.verdicts % SIZE_EVERY) {
116
+ const bytes = sizeOf(root);
117
+ if (bytes >= LARGE_RETICLE_BYTES)
118
+ return say(Moment.LARGE, `This project's .reticle holds ${String(Math.round(bytes / (1024 * 1024)))} MB of verification history on this machine alone.`);
119
+ }
120
+ return undefined;
121
+ }
122
+ async function flakyFlow(deps, root) {
123
+ try {
124
+ const parsed = FlakeFileSchema.safeParse(JSON.parse(await deps.fs.readFile(reticleDirPaths(root).flake)));
125
+ if (!parsed.success)
126
+ return undefined;
127
+ const found = Object.entries(parsed.data.flows).find(([, f]) => f.runs >= FLAKY_MIN_RUNS && 0 < f.fails && f.fails < f.runs);
128
+ return found === undefined
129
+ ? undefined
130
+ : { name: found[0], runs: found[1].runs, fails: found[1].fails };
131
+ }
132
+ catch {
133
+ return undefined;
134
+ }
135
+ }
136
+ /** Bytes under a directory. ponytail: a full walk, sampled every SIZE_EVERY verdicts. */
137
+ function sizeOf(dir) {
138
+ let total = 0;
139
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
140
+ const path = join(dir, entry.name);
141
+ total += entry.isDirectory() ? sizeOf(path) : statSync(path).size;
142
+ }
143
+ return total;
144
+ }
145
+ async function readTold(deps, root) {
146
+ try {
147
+ const raw = JSON.parse(await deps.fs.readFile(join(root, MOMENTS_FILE)));
148
+ const strings = (v) => Array.isArray(v) ? v.filter((m) => 'string' === typeof m) : [];
149
+ // The first version of this file was a bare list of moments.
150
+ if (Array.isArray(raw))
151
+ return { moments: strings(raw) };
152
+ const r = raw;
153
+ return {
154
+ moments: strings(r.moments),
155
+ ...('string' === typeof r.status ? { status: r.status } : {}),
156
+ };
157
+ }
158
+ catch {
159
+ return { moments: [] };
160
+ }
161
+ }
162
+ async function writeTold(deps, root, told) {
163
+ await deps.fs.writeFile(join(root, MOMENTS_FILE), JSON.stringify(told));
164
+ }
165
+ /** Tests only. */
166
+ export function resetPlatformMoments() {
167
+ seen.clear();
168
+ }
@@ -0,0 +1,7 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * `hint.testidFoundUnder` on a zero-match reticle_query. Declared here, not inline, because tools.ts
4
+ * is at its line cap; it must still be declared at all, since an undeclared field is stripped from
5
+ * structuredContent without a word.
6
+ */
7
+ export declare const TESTID_FOUND_UNDER_SCHEMA: z.ZodOptional<z.ZodString>;
@@ -0,0 +1,10 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * `hint.testidFoundUnder` on a zero-match reticle_query. Declared here, not inline, because tools.ts
4
+ * is at its line cap; it must still be declared at all, since an undeclared field is stripped from
5
+ * structuredContent without a word.
6
+ */
7
+ export const TESTID_FOUND_UNDER_SCHEMA = z
8
+ .string()
9
+ .optional()
10
+ .describe('A TESTID miss whose value IS on the page under another attribute, e.g. data-test-subj: set testIdAttribute in .reticle.json (Vite plugin) or pass it to connect().');
@@ -2,6 +2,7 @@
2
2
  * Read / record / replay tools — baselines + diff, recordings + replay, narrate, clock, state,
3
3
  * explore. Split out of tools.ts; assembled back via...READ_TOOLS.
4
4
  */
5
+ import { UNKEPT_EXPECT } from '../../language/flows/replay.js';
5
6
  import { resolveAnnotateTarget } from '../../language/flows/annotate-notes/annotate-target.js';
6
7
  import { z } from 'zod';
7
8
  import { EventType, ReticleCommand, REPLAY_PROGRAM_VERSION, SnapshotMode, StorageArea, } from '@reticlehq/core';
@@ -161,7 +162,7 @@ export const READ_TOOLS = [
161
162
  // Where the journey begins, so a saved flow can navigate here before step 1 instead of
162
163
  // replaying from wherever the page happens to be. Pathname only: a host or port belongs to
163
164
  // the machine that recorded it, not to the journey.
164
- deps.recordings.start(name, cursor, pathnameOf(session.url));
165
+ deps.recordings.start(name, cursor, pathnameOf(session.url), session.id);
165
166
  return Promise.resolve({ recordingName: name, since: cursor });
166
167
  },
167
168
  },
@@ -223,8 +224,13 @@ export const READ_TOOLS = [
223
224
  }
224
225
  : {};
225
226
  const unanchored = 0 < unstable ? unanchoredWarning(unstable) : undefined;
227
+ // A check that held and could not be saved, said rather than lost (#988).
228
+ const unkept = rec.steps.filter((st) => st.unkeptExpect !== undefined).length;
229
+ const lostChecks = 0 < unkept
230
+ ? `${String(unkept)} step(s) proved a consequence this flow does not keep: ${UNKEPT_EXPECT}.`
231
+ : undefined;
226
232
  const backtrack = recordingBacktrackWarning(routes);
227
- const warning = [unanchored, backtrack].filter((part) => part !== undefined);
233
+ const warning = [unanchored, backtrack, lostChecks].filter((part) => part !== undefined);
228
234
  const body = {
229
235
  recordingName: name,
230
236
  program,
@@ -19,11 +19,11 @@
19
19
  * the base64 4/3 inflation factor applied, so the encoded payload always fits in one WebSocket
20
20
  * frame.
21
21
  */
22
- import { ActionType, InputModeReason, ReticleCommand, TRANSPORT_LIMITS } from '@reticlehq/core';
23
- import { boxCenter, isPointerAction } from '../../portal/input/real-input.js';
22
+ import { ActionType, DriveErrorCode, InputModeReason, ReticleCommand, TRANSPORT_LIMITS, } from '@reticlehq/core';
23
+ import { boxCenter, DriveError, isRealInputAction, unspellablePressReason, } from '../../portal/input/real-input.js';
24
24
  import { assertDragNotDestructive, assertNotDestructive } from './act/act-danger.js';
25
25
  import { NATIVE_INPUT_ARG } from '@reticlehq/core';
26
- import { asRecord, asString } from '@reticlehq/core';
26
+ import { asNumber, asRecord, asString } from '@reticlehq/core';
27
27
  import { commandOrThrow } from './tool-kit.js';
28
28
  import { asBox } from './act/act-helpers.js';
29
29
  import { isAbsolute, join, relative, extname, basename, resolve } from 'node:path';
@@ -228,6 +228,13 @@ function synthetic(reason) {
228
228
  ? { result: undefined, settled: false }
229
229
  : { result: undefined, settled: false, reason };
230
230
  }
231
+ /**
232
+ * Filler for the provider signature, which demands a box on every action.
233
+ *
234
+ * No coordinate is derived from it and none is reported back: `performGesture` returns before it
235
+ * reads the box on a press, because a key addresses focus rather than a point.
236
+ */
237
+ const PRESS_HAS_NO_BOX = { x: 0, y: 0, width: 0, height: 0 };
231
238
  /**
232
239
  * Hover without a native pointer is a false success: synthetic mouseover reports dispatched and
233
240
  * settled while CSS `:hover` never applies. Same refusal shape as contenteditable — name the gap
@@ -258,8 +265,10 @@ async function hoverForReal(deps, session, ref, inner, provider) {
258
265
  if (providerReady && provider !== undefined) {
259
266
  try {
260
267
  const performed = await provider.perform(session.url, ActionType.HOVER, box, {});
268
+ // Hover always has a resolved box, so there is always a center to report: the provider's own
269
+ // reading, or the box's if it reported none. Both are the same point, not a fabricated one.
261
270
  if (performed.performed)
262
- return hoverSucceeded(performed.center);
271
+ return hoverSucceeded(performed.center ?? boxCenter(box));
263
272
  }
264
273
  catch {
265
274
  // A provider error used to fall back to synthetic dispatch. For hover that is a lie.
@@ -295,7 +304,14 @@ export async function tryRealInput(deps, session, ref, action, args) {
295
304
  // real debugging time, because the tool description promises a reason is "never silent".
296
305
  return askedForNative ? synthetic(InputModeReason.NOT_CONFIGURED) : synthetic();
297
306
  }
298
- if (!isPointerAction(action))
307
+ // Two `press`es this module will NOT drive, and neither is a fallback — the reason says which
308
+ // one was asked for, instead of a different gesture being sent. See `unspellablePressReason`.
309
+ if (action === ActionType.PRESS) {
310
+ const unspellable = unspellablePressReason(ref, inner);
311
+ if (unspellable !== undefined)
312
+ return synthetic(unspellable);
313
+ }
314
+ if (!isRealInputAction(action))
299
315
  return synthetic(InputModeReason.NOT_POINTER); // fill/type stay synthetic
300
316
  // "Don't click, run the code": a click/dblclick runs the occlusion-honest SYNTHETIC path by default
301
317
  // even with a provider configured — no coordinate gesture to be intercepted by the HUD or missed
@@ -307,11 +323,85 @@ export async function tryRealInput(deps, session, ref, action, args) {
307
323
  // Under version skew CDP is unusable while DOM tools still work (#688). Surface the skew sentence
308
324
  // rather than "page not correlated" / a silent provider-error fallback — those send the agent
309
325
  // hunting a dead context that is not dead.
310
- if (session.versionSkew !== undefined) {
326
+ //
327
+ // `press` is exempt, and it is the one action where throwing would take something away: a
328
+ // document key is dispatched entirely in the page, so it does not need CDP at all and has always
329
+ // worked under skew. Routing it into real input made it reach this line for the first time, and
330
+ // throwing here would turn a working synthetic press into a hard failure. It falls through to
331
+ // `isAvailableFor` instead, and a driver that really is dead answers PROVIDER_ERROR with the
332
+ // usual `fellBack` — still a signal, just not a refusal.
333
+ if (action !== ActionType.PRESS && session.versionSkew !== undefined) {
311
334
  throw new Error(session.versionSkew);
312
335
  }
313
336
  if (!(await provider.isAvailableFor(session.url)))
314
337
  return synthetic(InputModeReason.PAGE_NOT_CORRELATED);
338
+ const performArgs = {};
339
+ const value = asString(inner['value']);
340
+ if (value !== undefined)
341
+ performArgs.value = value;
342
+ const text = asString(inner['text']);
343
+ if (text !== undefined)
344
+ performArgs.text = text;
345
+ const key = asString(inner['key']);
346
+ if (key !== undefined)
347
+ performArgs.key = key;
348
+ const modifiers = inner['modifiers'];
349
+ if (Array.isArray(modifiers)) {
350
+ performArgs.modifiers = modifiers.filter((m) => 'string' === typeof m);
351
+ }
352
+ // A held key is the whole point of a hold-to-confirm control, and the synthetic path has honoured
353
+ // `holdMs` all along. Not forwarding it made the real keyboard the WEAKER of the two paths: it
354
+ // drove the key instantly and reported a hold that never held.
355
+ const holdMs = asNumber(inner['holdMs']);
356
+ if (holdMs !== undefined)
357
+ performArgs.holdMs = holdMs;
358
+ // `keys` and `code` are read but not forwarded: both are routed away above — a multi-key sequence
359
+ // and an explicit physical code have no driver spelling — so carrying them into the provider would
360
+ // only invite it to guess at a gesture the caller did not ask for.
361
+ // A document key has no element of its own: skip INSPECT and the box entirely.
362
+ if (action === ActionType.PRESS) {
363
+ try {
364
+ const performed = await provider.perform(session.url, action, PRESS_HAS_NO_BOX, performArgs);
365
+ if (!performed.performed)
366
+ return synthetic(InputModeReason.PROVIDER_DECLINED);
367
+ // No `center`: a key addresses focus, not a point. The placeholder's zero happened nowhere.
368
+ //
369
+ // `heldMs` IS forwarded, and it is the whole reason this object is not just the three fields
370
+ // above: `reticle_act`'s description promises `effect.heldMs` reports the hold actually
371
+ // achieved, and on the real path the act result IS the `effect` block. Dropping it here made
372
+ // that promise true on the synthetic path and silently false on the one this release added
373
+ // `holdMs` to — an agent checking for an armed hold-to-confirm control read `undefined`.
374
+ return {
375
+ result: {
376
+ performed: true,
377
+ action,
378
+ inputMode: performed.inputMode,
379
+ ...(performed.heldMs !== undefined ? { heldMs: performed.heldMs } : {}),
380
+ },
381
+ settled: true,
382
+ };
383
+ }
384
+ catch (error) {
385
+ // The ONE failure that must not fall back. Every other throw means the gesture did not
386
+ // happen, and the synthetic path is the honest answer. A release that failed means the
387
+ // gesture DID happen and the key may still be down — replaying it synthetically presses the
388
+ // same key a second time on top of a keyboard that is already holding something, which is
389
+ // the corruption the release exists to prevent. Refusing is the only answer that cannot
390
+ // make it worse.
391
+ if (error instanceof DriveError && error.code === DriveErrorCode.RELEASE_FAILED) {
392
+ throw error;
393
+ }
394
+ // No skew re-throw here, unlike the pointer path below: this action has a working synthetic
395
+ // route (see the skew check above), so the honest answer to a driver that threw is the
396
+ // fallback, not a refusal.
397
+ return {
398
+ result: undefined,
399
+ settled: false,
400
+ fellBack: true,
401
+ reason: InputModeReason.PROVIDER_ERROR,
402
+ };
403
+ }
404
+ }
315
405
  const inspected = await commandOrThrow(deps, session.id, ReticleCommand.INSPECT, { ref });
316
406
  assertNotDestructive(action, inner, inspected);
317
407
  const box = asBox(inspected);
@@ -332,13 +422,6 @@ export async function tryRealInput(deps, session, ref, action, args) {
332
422
  if (toBox === undefined)
333
423
  return synthetic(InputModeReason.DRAG_TARGET_UNRESOLVED);
334
424
  }
335
- const performArgs = {};
336
- const value = asString(inner['value']);
337
- if (value !== undefined)
338
- performArgs.value = value;
339
- const text = asString(inner['text']);
340
- if (text !== undefined)
341
- performArgs.text = text;
342
425
  if (toBox !== undefined)
343
426
  performArgs.toBox = toBox;
344
427
  try {
@@ -4,6 +4,7 @@
4
4
  * in its own leaf module (no dependency on any tool array) so the per-group tool files can import it
5
5
  * without a circular import — `tools.ts` assembles the groups and re-exports `ToolDef`/`ToolDeps`.
6
6
  */
7
+ import type { FirstRunWiring } from '../../portal/session/first-run-wiring.js';
7
8
  import { z } from 'zod';
8
9
  import { type ProjectId, ReticleCommand } from '@reticlehq/core';
9
10
  import type { SessionManager } from '../../portal/session/session-manager.js';
@@ -59,6 +60,25 @@ export interface ToolDeps<Ext = unknown> {
59
60
  * `tools.ts`, which keeps every existing `ToolDeps` construction working unchanged.
60
61
  */
61
62
  snapshots?: SnapshotCache;
63
+ /**
64
+ * The skew between THIS client's MCP server and the daemon, told once to this connection only.
65
+ *
66
+ * Per attach for the same reason as `snapshots`: one daemon serves every agent on the machine, and
67
+ * a daemon-wide queue handed one agent's "restart your MCP server" to whichever agent called next,
68
+ * whose server matched (#1136). Absent ⇒ this connection's server matched, or it did not announce.
69
+ */
70
+ peerSkew?: {
71
+ take(): string | undefined;
72
+ };
73
+ /**
74
+ * Who is asking: one id per MCP attach. A browser lease is handed back only to the attach that took
75
+ * it, so two agents on one origin never share a tab (#1226). Absent ⇒ every acquire gets its own.
76
+ */
77
+ attachId?: string;
78
+ /** Run one sync cycle with the platform now, for `reticle_project { push }`. Absent: not linked here. */
79
+ syncNow?: () => Promise<unknown>;
80
+ /** Wires the app on the agent's first use, instead of at installation. Absent: tell, do not wire. */
81
+ firstRun?: FirstRunWiring;
62
82
  /** cross-run outcome memory (.reticle/project.json). */
63
83
  project: ProjectStore;
64
84
  /** optional native-input provider. undefined ⇒ everything stays synthetic. */
@@ -80,7 +100,7 @@ export interface ToolDeps<Ext = unknown> {
80
100
  * embedding this engine may not have a filesystem to search. Absent ⇒ `reticleRoot`, which is
81
101
  * exactly today's behaviour.
82
102
  */
83
- artifactRootFor?: (projectId: ProjectId | undefined) => ArtifactRoot;
103
+ artifactRootFor?: (projectId: ProjectId | undefined, origin?: string) => ArtifactRoot;
84
104
  /**
85
105
  * A verification run just landed — wake cloud sync instead of waiting for its next tick.
86
106
  *
@@ -231,6 +251,10 @@ export declare const EnvelopeKey: {
231
251
  readonly VERSION_SKEW: "version_skew";
232
252
  /** A feedback report that was accepted and then failed to send. Only the reporter can act on it. */
233
253
  readonly FEEDBACK_UNDELIVERED: "feedback_undelivered";
254
+ /** Once per project: verified runs exist only on this machine. See platform-moment.ts. */
255
+ readonly PLATFORM: "platform";
256
+ /** The one thing to do next: declare the request, yield, fix sync, connect. See next-step.ts. */
257
+ readonly NEXT: "next";
234
258
  };
235
259
  export type EnvelopeKey = (typeof EnvelopeKey)[keyof typeof EnvelopeKey];
236
260
  /**
@@ -1,9 +1,3 @@
1
- /**
2
- * Shared kit for the MCP tool modules — the tool shape, the dependency bag, the common
3
- * session-id arg, and the two helpers (commandOrThrow, snapshotTree) every tool group needs. Lives
4
- * in its own leaf module (no dependency on any tool array) so the per-group tool files can import it
5
- * without a circular import — `tools.ts` assembles the groups and re-exports `ToolDef`/`ToolDeps`.
6
- */
7
1
  import { z } from 'zod';
8
2
  import { ReticleCommand, SnapshotMode } from '@reticlehq/core';
9
3
  import { normalizeLines } from '../../memory/project/baselines.js';
@@ -84,6 +78,10 @@ export const EnvelopeKey = {
84
78
  VERSION_SKEW: 'version_skew',
85
79
  /** A feedback report that was accepted and then failed to send. Only the reporter can act on it. */
86
80
  FEEDBACK_UNDELIVERED: 'feedback_undelivered',
81
+ /** Once per project: verified runs exist only on this machine. See platform-moment.ts. */
82
+ PLATFORM: 'platform',
83
+ /** The one thing to do next: declare the request, yield, fix sync, connect. See next-step.ts. */
84
+ NEXT: 'next',
87
85
  };
88
86
  /**
89
87
  * Fields that `runTool` / `withControl` splice onto EVERY session-bound tool result at runtime.
@@ -1,5 +1,7 @@
1
+ import { TESTID_FOUND_UNDER_SCHEMA } from './query-hint-schema.js';
1
2
  import { z } from 'zod';
2
- import { DiscoveryInvite, AttrNamesSchema, EventType, NoSessionAction, QueryBy, ReticleCommand, SnapshotMode, } from '@reticlehq/core';
3
+ import { EMPTY_SESSION_LIST_OUTPUT, emptySessionList } from './empty-session-list.js';
4
+ import { AttrNamesSchema, EventType, QueryBy, ReticleCommand, SnapshotMode } from '@reticlehq/core';
3
5
  import { ReticleTool } from '@reticlehq/core';
4
6
  import { withSizeCost } from '../../portal/session/output-budget.js';
5
7
  import { applySnapshotDelta, SnapshotCache } from './read/snapshot-delta.js';
@@ -82,6 +84,11 @@ export const RAW_TOOLS = [
82
84
  .string()
83
85
  .optional()
84
86
  .describe("Present only when this page's SDK version differs from the daemon's. A skewed pair connects and then disagrees about tool behaviour — usually surfacing as a bare -32000. Fix it before trusting any verdict from this session."),
87
+ window: z.string().optional().describe('Desktop window label (Tauri: main, setup…).'),
88
+ documentStatus: z
89
+ .number()
90
+ .optional()
91
+ .describe('Set only for an error page (4xx/5xx): not the app, never auto-selected first.'),
85
92
  lastSeenMs: z.number(),
86
93
  throttled: z.boolean(),
87
94
  focused: z.boolean(),
@@ -112,30 +119,7 @@ export const RAW_TOOLS = [
112
119
  .describe('Continuity of the SDK-to-daemon LINK for this tab, present for any tab the daemon has a record of: how long this attachment has lasted, how many times it dropped and came back, and when the last gap started and how long it lasted. `outages > 0` means a verdict over this window spans a gap where nothing was observed. These are link counters, NOT app health: they say nothing about whether the page origin is still serving, so a tab whose dev server has died reports `outages: 0` with a growing `connectedSinceMs` and is not evidence the app is up.'),
113
120
  }))
114
121
  .describe('Connected browser sessions with health state. `projectId` groups sessions by app (stable across port changes); `leased` marks a pool-managed headless context vs a human tab.'),
115
- why: z
116
- .string()
117
- .optional()
118
- .describe('Present ONLY when `sessions` is empty: why nothing is connected, and the next action that fixes it. An empty list is never the end of the road — read this before concluding the app cannot be driven.'),
119
- next_action: z
120
- .object({
121
- action: z.nativeEnum(NoSessionAction),
122
- command: z.string().optional(),
123
- port: z.number().optional(),
124
- reason: z.string(),
125
- })
126
- .optional()
127
- .describe("Present ONLY when `sessions` is empty: the same answer as `why`, executable. `command` is the LITERAL command to run, sourced from this project's own package.json scripts and lockfile — it is absent, never guessed, when the project declares no dev script. `action` is one of daemon_split | start_dev_server | run_init | open_app | reopen_app. `daemon_split` outranks the rest and means the app IS running and instrumented, on a DIFFERENT daemon than the one you are attached to — do not start or re-init anything, read `reason`."),
128
- talk_to_us: z
129
- .string()
130
- .optional()
131
- .describe('Present ONLY when `sessions` is empty: a call with the founder for the person you are working with. Offer it; never act on it.'),
132
- lastKnown: z
133
- .object({
134
- sessionId: z.string(),
135
- url: z.string(),
136
- })
137
- .optional()
138
- .describe('Present ONLY when `sessions` is empty AND a tab was connected to this daemon earlier: the last sessionId and URL that tab was on when it disappeared. A route that 500s tears the page down and the SDK never reconnects — this is the last thing we knew, not a route-500 verdict. An empty list with no lastKnown is a tab that never arrived.'),
122
+ ...EMPTY_SESSION_LIST_OUTPUT,
139
123
  },
140
124
  handler: async (deps) => {
141
125
  const provider = deps.realInput;
@@ -145,27 +129,9 @@ export const RAW_TOOLS = [
145
129
  realInputAvailable: provider !== undefined ? await provider.isAvailableFor(s.url) : false,
146
130
  leased: leasedIds.has(s.sessionId),
147
131
  })));
148
- // An empty list is the most common thing this tool ever returns, and on its own it is a dead
149
- // end: the agent asked the one question it knows to ask, got a confident-looking answer with
150
- // no next step, and stopped. The daemon already knows WHY nothing is connected — say it here
151
- // rather than only when a later tool fails for want of a session.
152
- if (0 === sessions.length) {
153
- const why = deps.sessions.noSessionHint();
154
- // The executable half. `why` is for the human reading the transcript; this is the one the
155
- // agent acts on, so it never has to parse a paragraph to find a command inside it.
156
- const next = deps.sessions.noSessionNextAction();
157
- // The last tab, when one was here. Optional-call: test stubs of SessionManager predate it.
158
- const known = deps.sessions.lastKnown?.();
159
- const lastKnown = undefined === known ? undefined : { sessionId: known.id, url: known.url };
160
- return {
161
- sessions,
162
- ...(why === undefined ? {} : { why }),
163
- ...(next === undefined ? {} : { next_action: next }),
164
- ...(undefined === lastKnown ? {} : { lastKnown }),
165
- // Setup that never connects is where most people give up; it is the talk worth having.
166
- talk_to_us: DiscoveryInvite.AGENT,
167
- };
168
- }
132
+ // An empty list is a dead end on its own; see empty-session-list.ts for what it carries.
133
+ if (0 === sessions.length)
134
+ return emptySessionList(deps, sessions);
169
135
  return { sessions };
170
136
  },
171
137
  },
@@ -400,6 +366,7 @@ export const RAW_TOOLS = [
400
366
  .array(z.string())
401
367
  .optional()
402
368
  .describe('Present when a ROLE+NAME search missed and that role DOES carry a nearly-matching name — role+name is exact, so "Mesh" does not find "2 Mesh". Retry with one of these spellings; no snapshot needed.'),
369
+ testidFoundUnder: TESTID_FOUND_UNDER_SCHEMA,
403
370
  })
404
371
  .optional()
405
372
  .describe('Present only on zero matches — tells you what IS on the page so you can diagnose the miss.'),
@@ -522,7 +489,10 @@ export const MERGE_PLANS = [
522
489
  // whatever comes first is what an agent discovers; when this sentence opened with `tune` the
523
490
  // MANDATORY handback sat past the cut and no agent reading the catalogue ever learned it
524
491
  // existed. The rule is: if a tool has an obligation, the obligation goes first.
525
- 'Session lifecycle and the human channel, by action: "yield" hands control back to the human and is MANDATORY before you stop driving (mode: waiting|ask, between turns); "tune" adjusts the presenter session (e.g. idle-end window); "end" terminates the session for good; "resume" clears a human pause; "messages" drains the human→agent inbox; "review" lists/resolves the mistakes a human pinned to elements; "narrate" states your intent on the presenter HUD.',
492
+ // The first sentence is all a lean surface shows (160 characters), so it carries the
493
+ // obligation and the HUD control; the rest is the detail for a full one.
494
+ 'By action: "yield" is MANDATORY before you stop driving; "tune" hides or moves the HUD, or sets the idle window. ' +
495
+ 'In full: "yield" hands control back to the human (mode: waiting|ask, between turns); "tune" takes `hud` (hidden, shown, removed until reload, or a corner) for when the HUD covers a control you must test, and `idleEndMs`; "end" terminates the session for good; "resume" clears a human pause; "messages" drains the human→agent inbox; "review" lists/resolves the mistakes a human pinned to elements; "narrate" states your intent on the presenter HUD.',
526
496
  members: {
527
497
  tune: ReticleTool.SESSION_TUNE,
528
498
  yield: ReticleTool.YIELD,
@@ -538,7 +508,7 @@ export const MERGE_PLANS = [
538
508
  },
539
509
  {
540
510
  name: ReticleTool.VERIFY,
541
- description: 'What is proved, and what is not — by action. Start from a change: { action: "affected" } names which saved flows must re-verify for the files you edited (pass `files`, and/or `since` for a git ref), and "change" goes further and actually replays them, answering with one `verified` plus `because`. `unknown` there is the honest answer when NO saved flow covers the change: nothing ran, so nothing was proved, and it is never reported as green. Without a change to start from: "flows" replays every saved flow for one consolidated suite verdict (deterministic, no LLM per flow), and "coverage" lists the interactive controls you have and have NOT driven this session — an untouched list still holding the controls your change affects means you are not done. "crawl" is the no-script option: it drives every reachable control itself and reports single-channel faults (console errors, failed requests, dead controls) and CONTRADICTIONS, two channels disagreeing about the same click — the false greens a human cannot see, because a human watches the screen and the screen looks correct. "explore" is the one to reach for on a project with NO saved flows: a model inside the daemon drives the app for you — pass a `persona` and it completes that whole journey — and RECORDS what it drove, so the next run replays it with no model in the loop and none of the driving costs your context. "mutate" grades the FLOW rather than the app: it breaks the endpoint a saved flow declared it depends on and replays it, and a flow that stays GREEN through a broken subject is a click sequence, not a test. "heal" repairs a flow whose LOCATOR drifted (a renamed testid), re-asserting the saved consequence before it writes and refusing when that stops firing — it heals a locator, never an intent. DESTRUCTIVE: crawl and explore really click, and may navigate or mutate state; "mutate" really fails a request on a driven page, and always puts it back.',
511
+ description: 'What is proved, and what is not — by action. Start from a change: { action: "affected" } names which saved flows must re-verify for the files you edited (pass `files`, and/or `since` for a git ref), and "change" goes further and actually replays them, answering with one `verified` plus `because`. `unknown` there is the honest answer when NO saved flow covers the change: nothing ran, so nothing was proved, and it is never reported as green. Without a change to start from: "flows" replays every saved flow for one consolidated suite verdict (deterministic, no LLM per flow), and "coverage" lists the interactive controls you have and have NOT driven this session — an untouched list still holding the controls your change affects means you are not done. "crawl" is the no-script option: it drives every reachable control itself and reports single-channel faults (console errors, failed requests, dead controls) and CONTRADICTIONS, two channels disagreeing about the same click — the false greens a human cannot see, because a human watches the screen and the screen looks correct. "explore" is the one to reach for on a project with NO saved flows: the Reticle Harness drives the app for you on the platform — pass a `persona` and it completes that whole journey — and RECORDS what it drove, so the next run replays it with no model in the loop and none of the driving costs your context. "mutate" grades the FLOW rather than the app: it breaks the endpoint a saved flow declared it depends on and replays it, and a flow that stays GREEN through a broken subject is a click sequence, not a test. "heal" repairs a flow whose LOCATOR drifted (a renamed testid), re-asserting the saved consequence before it writes and refusing when that stops firing — it heals a locator, never an intent. DESTRUCTIVE: crawl and explore really click, and may navigate or mutate state; "mutate" really fails a request on a driven page, and always puts it back.',
542
512
  members: {
543
513
  change: ReticleTool.VERIFY_CHANGE,
544
514
  flows: ReticleTool.FLOW_VERIFY,
@@ -804,6 +774,8 @@ export const MERGED_SURFACE_PLANS = [
804
774
  // surface: on the default one `reticle_sessions` still exists and this tool is purely
805
775
  // lifecycle, where no member is the obvious bare meaning.
806
776
  defaultAction: 'list',
777
+ description: 'By action: "list" (the default) shows connected tabs; "yield" is MANDATORY before you stop; "tune" hides or moves the HUD; "feedback" reports a problem. ' +
778
+ 'In full: "list" is the first call to make, and on a project that has never connected it wires the app and names the files it changed under `wired`; "yield" hands control back to the human (mode: waiting|ask); "tune" takes `hud` (hidden, shown, removed until reload, or a corner) for when the HUD covers a control you must test, and `idleEndMs`; "feedback" files a report; "end", "resume", "messages", "review" and "narrate" manage the session and the human channel.',
807
779
  }
808
780
  : plan),
809
781
  ...SURFACE_MERGE_PLANS,
@@ -27,7 +27,7 @@ import { ActionType, FeedbackKind, QueryBy, SnapshotMode } from '@reticlehq/core
27
27
  const SAFE_ENUM_PARAMS = new Map([
28
28
  // Every entry DERIVED from the enum core already owns, never hand-listed. The first version listed
29
29
  // them literally and was wrong on the day it shipped, not merely at risk: `action` was missing
30
- // `fill`, `dblclick`, `submit`, `scrollIntoView`, `drag` and `webmcp` — so `fill`, one of the most
30
+ // `fill`, `dblclick`, `submit`, `scrollIntoView` and `drag` — so `fill`, one of the most
31
31
  // common actions there is, silently reported as `other` — while carrying a `scroll` that
32
32
  // `ActionType` does not define. `mode` mixed SnapshotMode with unrelated session-lifecycle values.
33
33
  // Deriving is not tidiness here; it is the difference between the data being right and being wrong.
@@ -19,6 +19,8 @@ export interface HudSummary {
19
19
  hudJourney?: string[];
20
20
  /** Present, and true, only when the journey was cut. */
21
21
  hudJourneyCut?: true;
22
+ /** Times each rail slide became the visible one. Slide ids are Reticle's own, never page text. */
23
+ hudSlides?: Record<string, number>;
22
24
  }
23
25
  export declare class HudMetrics {
24
26
  #private;