@reticlehq/server 3.5.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 (328) 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/cli-flow-commands.js +4 -8
  10. package/dist/command/cli/cli-parse.d.ts +1 -1
  11. package/dist/command/cli/cli-parse.js +10 -10
  12. package/dist/command/cli/cli-verify.d.ts +1 -1
  13. package/dist/command/cli/cli-verify.js +35 -25
  14. package/dist/command/cli/cloud-cli.js +51 -12
  15. package/dist/command/cli/cloud-login.d.ts +8 -0
  16. package/dist/command/cli/cloud-login.js +36 -2
  17. package/dist/command/cli/demo/demo-page.js +11 -3
  18. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +46 -0
  19. package/dist/command/cli/doctor/browser/chromium-hint.js +99 -1
  20. package/dist/command/cli/drive/drive-attach.d.ts +27 -2
  21. package/dist/command/cli/drive/drive-attach.js +71 -4
  22. package/dist/command/cli/drive/drive-command.d.ts +2 -1
  23. package/dist/command/cli/drive/drive-command.js +13 -7
  24. package/dist/command/cli/lifecycle/daemon-lifecycle.js +4 -1
  25. package/dist/command/cli/ports/port-holder.d.ts +10 -0
  26. package/dist/command/cli/ports/port-holder.js +24 -0
  27. package/dist/command/cli/setup-mcp-cli.js +6 -4
  28. package/dist/command/cli/tutorial.d.ts +2 -2
  29. package/dist/command/cli/tutorial.js +6 -9
  30. package/dist/command/cli.d.ts +6 -0
  31. package/dist/command/cli.js +28 -22
  32. package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
  33. package/dist/command/daemon/binding/daemon-status-probe.js +1 -0
  34. package/dist/command/daemon/daemon-resolve.d.ts +14 -0
  35. package/dist/command/daemon/daemon-resolve.js +33 -2
  36. package/dist/command/setup/agent-writer.d.ts +1 -1
  37. package/dist/command/setup/agent-writer.js +3 -2
  38. package/dist/command/setup/bringup/connect-lease.d.ts +1 -0
  39. package/dist/command/setup/bringup/connect-lease.js +1 -0
  40. package/dist/command/setup/bringup/dev-server-wait.d.ts +8 -0
  41. package/dist/command/setup/bringup/dev-server-wait.js +19 -0
  42. package/dist/command/setup/init/init-port.d.ts +4 -0
  43. package/dist/command/setup/init/init-port.js +10 -0
  44. package/dist/command/setup/init/init-runtime.d.ts +10 -0
  45. package/dist/command/setup/init/init-runtime.js +18 -1
  46. package/dist/command/setup/node-effects.js +2 -1
  47. package/dist/command/setup/remaining-steps.js +2 -1
  48. package/dist/command/setup/run-setup.d.ts +5 -0
  49. package/dist/command/setup/run-setup.js +35 -27
  50. package/dist/command/setup/setup-command.d.ts +2 -1
  51. package/dist/command/setup/setup-command.js +2 -0
  52. package/dist/command/version/version-skew.js +11 -2
  53. package/dist/features/crawl/crawl-tools.js +4 -0
  54. package/dist/features/crawl/crawl.d.ts +6 -0
  55. package/dist/features/crawl/crawl.js +9 -1
  56. package/dist/features/exhaust/ledger.d.ts +7 -0
  57. package/dist/features/exhaust/ledger.js +35 -9
  58. package/dist/features/harness/app-findings.d.ts +2 -0
  59. package/dist/features/harness/app-findings.js +52 -0
  60. package/dist/features/harness/drive-report.d.ts +27 -1
  61. package/dist/features/harness/drive-report.js +162 -11
  62. package/dist/features/harness/drivers.d.ts +10 -14
  63. package/dist/features/harness/drivers.js +10 -14
  64. package/dist/features/harness/fill-values.d.ts +3 -73
  65. package/dist/features/harness/fill-values.js +3 -169
  66. package/dist/features/harness/goals.d.ts +21 -0
  67. package/dist/features/harness/goals.js +77 -0
  68. package/dist/features/harness/harness.d.ts +21 -0
  69. package/dist/features/harness/harness.js +69 -2
  70. package/dist/features/harness/platform/drive-target.d.ts +30 -0
  71. package/dist/features/harness/platform/drive-target.js +81 -0
  72. package/dist/features/harness/platform/local-apps.d.ts +94 -0
  73. package/dist/features/harness/platform/local-apps.js +204 -0
  74. package/dist/features/harness/platform/remote-drive.d.ts +180 -0
  75. package/dist/features/harness/platform/remote-drive.js +365 -0
  76. package/dist/features/harness/platform/script.d.ts +48 -0
  77. package/dist/features/harness/platform/script.js +128 -0
  78. package/dist/features/harness/platform/server-driver.d.ts +41 -0
  79. package/dist/features/harness/platform/server-driver.js +179 -0
  80. package/dist/features/harness/platform-config.d.ts +7 -0
  81. package/dist/features/harness/platform-config.js +14 -2
  82. package/dist/features/harness/script/app-controls.d.ts +5 -0
  83. package/dist/features/harness/script/app-controls.js +40 -0
  84. package/dist/features/harness/script/lane-ids.d.ts +14 -0
  85. package/dist/features/harness/script/lane-ids.js +16 -0
  86. package/dist/features/harness/script/script-run.d.ts +41 -0
  87. package/dist/features/harness/script/script-run.js +299 -0
  88. package/dist/features/license/license.d.ts +2 -2
  89. package/dist/features/visual/visual-tools.d.ts +10 -1
  90. package/dist/features/visual/visual-tools.js +40 -3
  91. package/dist/hooks/driven-by.d.ts +4 -0
  92. package/dist/hooks/driven-by.js +16 -0
  93. package/dist/hooks/hook-commands.js +15 -4
  94. package/dist/hooks/tool-log.d.ts +9 -0
  95. package/dist/hooks/tool-log.js +29 -0
  96. package/dist/index.d.ts +1 -0
  97. package/dist/index.js +76 -82
  98. package/dist/judgement/runs/artifact/build-verification-run.js +6 -0
  99. package/dist/judgement/runs/artifact/replay-mapping.d.ts +1 -1
  100. package/dist/judgement/runs/artifact/replay-mapping.js +16 -6
  101. package/dist/judgement/runs/artifact/run-store.d.ts +1 -1
  102. package/dist/judgement/runs/artifact/run-store.js +26 -1
  103. package/dist/judgement/runs/drive-run.d.ts +22 -0
  104. package/dist/judgement/runs/drive-run.js +99 -1
  105. package/dist/judgement/runs/repair-prompt.js +1 -1
  106. package/dist/judgement/runs/reticle-runner.js +8 -1
  107. package/dist/judgement/runs/verification-sync.d.ts +3 -2
  108. package/dist/judgement/runs/verification-sync.js +4 -3
  109. package/dist/language/flows/change/gate.d.ts +10 -0
  110. package/dist/language/flows/change/gate.js +22 -0
  111. package/dist/language/flows/decision.js +34 -0
  112. package/dist/language/flows/flow-anchor.d.ts +54 -8
  113. package/dist/language/flows/flow-anchor.js +88 -3
  114. package/dist/language/flows/flow-cross-step.d.ts +15 -0
  115. package/dist/language/flows/flow-cross-step.js +21 -0
  116. package/dist/language/flows/flow-learning.js +8 -11
  117. package/dist/language/flows/flow-mutate-tools.js +5 -3
  118. package/dist/language/flows/flow-replay-run.d.ts +23 -9
  119. package/dist/language/flows/flow-replay-run.js +101 -94
  120. package/dist/language/flows/flow-replay.d.ts +23 -1
  121. package/dist/language/flows/flow-replay.js +92 -19
  122. package/dist/language/flows/flow-scope.js +5 -1
  123. package/dist/language/flows/flow-step-runners.d.ts +20 -2
  124. package/dist/language/flows/flow-step-runners.js +236 -42
  125. package/dist/language/flows/flow-store-for-session.d.ts +2 -2
  126. package/dist/language/flows/flow-store-for-session.js +2 -2
  127. package/dist/language/flows/flow-tools.js +58 -34
  128. package/dist/language/flows/flows.js +5 -2
  129. package/dist/language/flows/heal-run.js +6 -4
  130. package/dist/language/flows/recording/flake.d.ts +2 -2
  131. package/dist/language/flows/recording/tape/recordings.d.ts +30 -4
  132. package/dist/language/flows/recording/tape/recordings.js +40 -8
  133. package/dist/language/flows/replay-from-hud.d.ts +20 -0
  134. package/dist/language/flows/replay-from-hud.js +51 -0
  135. package/dist/language/flows/replay.d.ts +9 -5
  136. package/dist/language/flows/replay.js +27 -9
  137. package/dist/language/flows/suite/suite-flakes.js +2 -0
  138. package/dist/language/flows/suite-selection.d.ts +2 -15
  139. package/dist/language/flows/suite-selection.js +4 -3
  140. package/dist/language/flows/verify-change-tools.js +8 -4
  141. package/dist/launch-chromium.d.ts +27 -5
  142. package/dist/launch-chromium.js +38 -8
  143. package/dist/memory/cloud/cloud-config.d.ts +12 -0
  144. package/dist/memory/cloud/cloud-config.js +15 -0
  145. package/dist/memory/cloud/cloud-sync.d.ts +17 -14
  146. package/dist/memory/cloud/cloud-sync.js +12 -1
  147. package/dist/memory/cloud/harness-config.d.ts +26 -1
  148. package/dist/memory/cloud/harness-config.js +99 -4
  149. package/dist/memory/cloud/hud-notices-source.d.ts +21 -0
  150. package/dist/memory/cloud/hud-notices-source.js +128 -0
  151. package/dist/memory/cloud/memory-scope.d.ts +144 -0
  152. package/dist/memory/cloud/memory-scope.js +205 -0
  153. package/dist/memory/cloud/sync-cycle.d.ts +12 -3
  154. package/dist/memory/cloud/sync-cycle.js +16 -3
  155. package/dist/memory/cloud/sync-daemon.js +7 -5
  156. package/dist/memory/cloud/sync-disk.d.ts +1 -1
  157. package/dist/memory/cloud/sync-disk.js +5 -8
  158. package/dist/memory/impact/account-watch.d.ts +19 -0
  159. package/dist/memory/impact/account-watch.js +34 -0
  160. package/dist/memory/impact/impact-recorder.d.ts +8 -0
  161. package/dist/memory/impact/impact-recorder.js +12 -2
  162. package/dist/memory/impact/impact-store.d.ts +16 -0
  163. package/dist/memory/impact/impact-store.js +142 -26
  164. package/dist/memory/intent/intent-shard.d.ts +2 -2
  165. package/dist/memory/intent/intent-tools.js +80 -2
  166. package/dist/memory/journal/envelope.d.ts +8 -8
  167. package/dist/memory/journal/journal-recorder.js +7 -1
  168. package/dist/memory/journal/on-disk/retention.d.ts +3 -0
  169. package/dist/memory/journal/on-disk/retention.js +5 -0
  170. package/dist/memory/journal/on-disk/startup-maintenance.js +3 -1
  171. package/dist/memory/journal/on-disk/workspace-tiers.js +7 -4
  172. package/dist/memory/journal/session-end.js +6 -4
  173. package/dist/memory/project/artifact-root-resolver.d.ts +26 -1
  174. package/dist/memory/project/artifact-root-resolver.js +94 -13
  175. package/dist/memory/project/artifact-root.d.ts +42 -5
  176. package/dist/memory/project/artifact-root.js +87 -8
  177. package/dist/memory/project/dir/reticle-dir.d.ts +4 -2
  178. package/dist/memory/project/dir/reticle-dir.js +3 -1
  179. package/dist/memory/project/project-tools.js +22 -3
  180. package/dist/memory/project/session-root.d.ts +24 -1
  181. package/dist/memory/project/session-root.js +41 -5
  182. package/dist/memory/project/sync-status.d.ts +39 -0
  183. package/dist/memory/project/sync-status.js +90 -0
  184. package/dist/memory/recall/memory-tools.js +2 -0
  185. package/dist/memory/recall/project-memory.d.ts +19 -0
  186. package/dist/memory/recall/project-memory.js +45 -6
  187. package/dist/portal/bridge/bridge.d.ts +3 -1
  188. package/dist/portal/bridge/bridge.js +19 -2
  189. package/dist/portal/bridge/pairing-token.d.ts +9 -0
  190. package/dist/portal/bridge/pairing-token.js +17 -0
  191. package/dist/portal/input/real-input.d.ts +81 -10
  192. package/dist/portal/input/real-input.js +207 -22
  193. package/dist/portal/input/viewport-tools.js +3 -3
  194. package/dist/portal/pool/browser-pool.d.ts +22 -9
  195. package/dist/portal/pool/browser-pool.js +70 -35
  196. package/dist/portal/pool/playwright-launcher.d.ts +13 -0
  197. package/dist/portal/pool/playwright-launcher.js +17 -1
  198. package/dist/portal/pool/pool-contract.d.ts +7 -4
  199. package/dist/portal/session/command-payload.d.ts +8 -0
  200. package/dist/portal/session/command-payload.js +19 -0
  201. package/dist/portal/session/dev-server/dev-command.d.ts +12 -0
  202. package/dist/portal/session/dev-server/dev-command.js +61 -12
  203. package/dist/portal/session/facts/departure-note.d.ts +22 -0
  204. package/dist/portal/session/facts/departure-note.js +47 -0
  205. package/dist/portal/session/facts/session-replaced.d.ts +5 -0
  206. package/dist/portal/session/facts/session-replaced.js +5 -0
  207. package/dist/portal/session/first-run-wiring.d.ts +42 -0
  208. package/dist/portal/session/first-run-wiring.js +132 -0
  209. package/dist/portal/session/human/notes-ledger.d.ts +20 -0
  210. package/dist/portal/session/human/notes-ledger.js +55 -0
  211. package/dist/portal/session/human/review-store.d.ts +12 -0
  212. package/dist/portal/session/human/review-store.js +18 -10
  213. package/dist/portal/session/live-control-tools.d.ts +0 -13
  214. package/dist/portal/session/live-control-tools.js +28 -3
  215. package/dist/portal/session/no-session-diagnosis.d.ts +7 -0
  216. package/dist/portal/session/no-session-diagnosis.js +15 -0
  217. package/dist/portal/session/no-session-next-action.d.ts +11 -0
  218. package/dist/portal/session/no-session-next-action.js +31 -5
  219. package/dist/portal/session/no-session-watch.d.ts +14 -1
  220. package/dist/portal/session/no-session-watch.js +63 -10
  221. package/dist/portal/session/presence/session-mirror.js +3 -0
  222. package/dist/portal/session/serving-directory.d.ts +27 -0
  223. package/dist/portal/session/serving-directory.js +45 -0
  224. package/dist/portal/session/session-health.d.ts +7 -0
  225. package/dist/portal/session/session-health.js +6 -0
  226. package/dist/portal/session/session-info.d.ts +8 -1
  227. package/dist/portal/session/session-info.js +6 -0
  228. package/dist/portal/session/session-manager.d.ts +21 -0
  229. package/dist/portal/session/session-manager.js +44 -8
  230. package/dist/portal/session/session-reaper.js +4 -1
  231. package/dist/portal/session/session-successor.d.ts +5 -0
  232. package/dist/portal/session/session-tools.js +26 -3
  233. package/dist/portal/session/session.d.ts +7 -2
  234. package/dist/portal/session/session.js +21 -18
  235. package/dist/start-options.d.ts +6 -0
  236. package/dist/status-payload.d.ts +10 -0
  237. package/dist/status-payload.js +23 -0
  238. package/dist/surface/http-server.js +11 -1
  239. package/dist/surface/mcp/mcp-proxy.d.ts +5 -0
  240. package/dist/surface/mcp/mcp-proxy.js +36 -5
  241. package/dist/surface/mcp/mcp.d.ts +11 -6
  242. package/dist/surface/mcp/mcp.js +27 -13
  243. package/dist/surface/mcp/server-instructions.d.ts +4 -2
  244. package/dist/surface/mcp/server-instructions.js +14 -4
  245. package/dist/surface/mcp/shared-params.js +2 -2
  246. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +8 -0
  247. package/dist/surface/tools/act/navigation/navigate-arrival.js +56 -2
  248. package/dist/surface/tools/act/settle-in-flight.js +25 -3
  249. package/dist/surface/tools/act-sequence-tool.js +2 -2
  250. package/dist/surface/tools/act-tools.js +23 -7
  251. package/dist/surface/tools/assert/assert-verdict.d.ts +1 -1
  252. package/dist/surface/tools/assert/assert-verdict.js +17 -2
  253. package/dist/surface/tools/browser-tools.js +8 -1
  254. package/dist/surface/tools/chat-drives.d.ts +26 -0
  255. package/dist/surface/tools/chat-drives.js +165 -0
  256. package/dist/surface/tools/empty-session-list.d.ts +41 -0
  257. package/dist/surface/tools/empty-session-list.js +80 -0
  258. package/dist/surface/tools/error-recovery.d.ts +6 -0
  259. package/dist/surface/tools/error-recovery.js +37 -4
  260. package/dist/surface/tools/explore-tools.d.ts +10 -1
  261. package/dist/surface/tools/explore-tools.js +82 -13
  262. package/dist/surface/tools/feedback-tools.js +14 -0
  263. package/dist/surface/tools/harness-explore.d.ts +96 -14
  264. package/dist/surface/tools/harness-explore.js +227 -206
  265. package/dist/surface/tools/harness-plan.d.ts +12 -0
  266. package/dist/surface/tools/harness-plan.js +31 -0
  267. package/dist/surface/tools/harness-script.d.ts +8 -0
  268. package/dist/surface/tools/harness-script.js +342 -0
  269. package/dist/surface/tools/harness-toolset.d.ts +14 -1
  270. package/dist/surface/tools/harness-toolset.js +77 -6
  271. package/dist/surface/tools/invoke-tool.js +52 -1
  272. package/dist/surface/tools/lease-seed.d.ts +29 -0
  273. package/dist/surface/tools/lease-seed.js +123 -0
  274. package/dist/surface/tools/lease-tools.d.ts +3 -23
  275. package/dist/surface/tools/lease-tools.js +30 -131
  276. package/dist/surface/tools/live-call-text.js +2 -2
  277. package/dist/surface/tools/next-step.d.ts +19 -0
  278. package/dist/surface/tools/next-step.js +159 -0
  279. package/dist/surface/tools/observe-tools.js +2 -4
  280. package/dist/surface/tools/platform-moment.d.ts +12 -0
  281. package/dist/surface/tools/platform-moment.js +168 -0
  282. package/dist/surface/tools/query-hint-schema.d.ts +7 -0
  283. package/dist/surface/tools/query-hint-schema.js +10 -0
  284. package/dist/surface/tools/read-tools.js +8 -2
  285. package/dist/surface/tools/real-input-attempt.js +96 -13
  286. package/dist/surface/tools/tool-kit.d.ts +15 -1
  287. package/dist/surface/tools/tool-kit.js +4 -6
  288. package/dist/surface/tools/tools.js +20 -48
  289. package/dist/telemetry/hud-metrics.d.ts +2 -0
  290. package/dist/telemetry/hud-metrics.js +17 -0
  291. package/dist/telemetry/telemetry.d.ts +12 -0
  292. package/dist/telemetry/telemetry.js +43 -14
  293. package/dist/telemetry/verification-of.js +1 -0
  294. package/dist/wire-journal.js +4 -1
  295. package/docs/README.md +1 -1
  296. package/docs/agent-cheatsheet.md +1 -1
  297. package/docs/autodrive.md +29 -25
  298. package/docs/cli/doctor.mdx +6 -0
  299. package/docs/cli/drive.mdx +8 -2
  300. package/docs/cli/init.mdx +11 -13
  301. package/docs/cli/status.mdx +5 -1
  302. package/docs/cli/version.mdx +3 -3
  303. package/docs/cli.mdx +1 -1
  304. package/docs/desktop.mdx +5 -1
  305. package/docs/environment.mdx +3 -7
  306. package/docs/getting-started.md +13 -11
  307. package/docs/human-control.md +1 -2
  308. package/docs/index.mdx +4 -3
  309. package/docs/install-manual.mdx +2 -2
  310. package/docs/packages/tauri.mdx +2 -2
  311. package/docs/packages/vite-plugin.mdx +22 -1
  312. package/docs/packages.mdx +1 -1
  313. package/docs/quickstart.mdx +17 -7
  314. package/docs/telemetry.md +1 -1
  315. package/docs/tools/session-and-feedback.mdx +9 -9
  316. package/docs/usage.md +5 -2
  317. package/docs/what-is-recorded.md +13 -3
  318. package/docs/what-is-reticle.mdx +3 -1
  319. package/package.json +8 -12
  320. package/dist/features/harness/driver.d.ts +0 -50
  321. package/dist/features/harness/driver.js +0 -236
  322. package/dist/features/harness/jev-driver.d.ts +0 -188
  323. package/dist/features/harness/jev-driver.js +0 -678
  324. package/dist/features/harness/openai-driver.d.ts +0 -58
  325. package/dist/features/harness/openai-driver.js +0 -206
  326. package/dist/memory/project/dir/fill-value-store.d.ts +0 -32
  327. package/dist/memory/project/dir/fill-value-store.js +0 -88
  328. package/docs/local-registry.md +0 -143
@@ -1,4 +1,13 @@
1
- import type { ToolDef } from '../../surface/tools/tool-kit.js';
1
+ import type { ToolDef, ToolDeps } from '../../surface/tools/tool-kit.js';
2
+ /**
3
+ * One picture of the tab a chat-requested drive is driving, or undefined when there is none to take.
4
+ *
5
+ * The leased browser first, when this daemon launched the tab; then the desktop window's own capture,
6
+ * the same route `reticle_screenshot` takes. Without the second, a drive of a desktop app sent its
7
+ * steps to the chat and never a picture. Never throws: a frame is a courtesy, and a tab that has
8
+ * gone mid-drive must not break the drive.
9
+ */
10
+ export declare function driveFrame(deps: ToolDeps, sessionId: string, jpegQuality: number): Promise<Uint8Array | undefined>;
2
11
  /**
3
12
  * Only read a capture the shell wrote. The path arrives from the PAGE, and the daemon must not
4
13
  * become a file-read oracle for whatever a compromised renderer names.
@@ -111,6 +111,32 @@ async function desktopCapture(deps, sessionId, fullPage) {
111
111
  return {};
112
112
  }
113
113
  }
114
+ /**
115
+ * One picture of the tab a chat-requested drive is driving, or undefined when there is none to take.
116
+ *
117
+ * The leased browser first, when this daemon launched the tab; then the desktop window's own capture,
118
+ * the same route `reticle_screenshot` takes. Without the second, a drive of a desktop app sent its
119
+ * steps to the chat and never a picture. Never throws: a frame is a courtesy, and a tab that has
120
+ * gone mid-drive must not break the drive.
121
+ */
122
+ export async function driveFrame(deps, sessionId, jpegQuality) {
123
+ try {
124
+ const leased = await deps.pool?.screenshotLease(sessionId, { jpegQuality });
125
+ if (leased !== undefined)
126
+ return leased;
127
+ // The window `reticle drive` opened: neither a lease nor a desktop shell, but its page is ours.
128
+ const url = deps.sessions.resolve(sessionId).url;
129
+ if (true === (await deps.realInput?.isAvailableFor(url))) {
130
+ const shot = await deps.realInput?.screenshot?.(url, { jpegQuality });
131
+ if (shot !== undefined)
132
+ return shot;
133
+ }
134
+ return (await desktopCapture(deps, sessionId, false)).png;
135
+ }
136
+ catch {
137
+ return undefined;
138
+ }
139
+ }
114
140
  /**
115
141
  * Only read a capture the shell wrote. The path arrives from the PAGE, and the daemon must not
116
142
  * become a file-read oracle for whatever a compromised renderer names.
@@ -179,9 +205,20 @@ function runtimeOf(deps, sessionId) {
179
205
  * baseline that says nothing about the content below the fold, and a later diff would call it green.
180
206
  */
181
207
  async function capture(deps, sessionId, args) {
182
- const provider = screenshotProvider(deps);
183
- if (provider !== undefined) {
184
- const session = deps.sessions.resolve(sessionId);
208
+ /*
209
+ * Only a provider driving THIS session's page. A launched provider photographs the one page it
210
+ * owns whatever url it is handed, so asking it about another session saved the driven tab's pixels
211
+ * under that session's name and answered `saved: true` (#1407). Real input asks the same question
212
+ * before every gesture.
213
+ */
214
+ const candidate = screenshotProvider(deps);
215
+ const session = candidate === undefined ? undefined : deps.sessions.resolve(sessionId);
216
+ const provider = candidate !== undefined &&
217
+ session !== undefined &&
218
+ (await candidate.isAvailableFor(session.url))
219
+ ? candidate
220
+ : undefined;
221
+ if (provider !== undefined && session !== undefined) {
185
222
  const png = await provider.screenshot(session.url, await buildOpts(deps, sessionId, args));
186
223
  // A driven browser renders the session's URL in a BROWSER — so the pixels are web even when the
187
224
  // session named is a desktop window. Scoping those under the desktop runtime would corrupt that
@@ -0,0 +1,4 @@
1
+ import type { DrivenBy } from '@reticlehq/core';
2
+ export declare function runDrivenBy<T>(by: DrivenBy, fn: () => Promise<T>): Promise<T>;
3
+ /** The Harness drive this call is part of, or undefined when the connected agent made it. */
4
+ export declare function currentDrivenBy(): DrivenBy | undefined;
@@ -0,0 +1,16 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ /**
3
+ * Which Harness drive the current tool call belongs to, carried across every await under it.
4
+ *
5
+ * The Harness dispatches through the same tools an agent calls, so nothing in a tool's arguments can
6
+ * say who called it, and adding a field to every tool's schema would advertise it to every agent.
7
+ * The context is set once around each Harness call and read where the journal writes the action.
8
+ */
9
+ const current = new AsyncLocalStorage();
10
+ export function runDrivenBy(by, fn) {
11
+ return current.run(by, fn);
12
+ }
13
+ /** The Harness drive this call is part of, or undefined when the connected agent made it. */
14
+ export function currentDrivenBy() {
15
+ return current.getStore();
16
+ }
@@ -21,7 +21,7 @@
21
21
  * broken gets a log line; their verdicts keep working.
22
22
  */
23
23
  import { spawn } from 'node:child_process';
24
- import { readFileSync, statSync } from 'node:fs';
24
+ import { closeSync, fstatSync, openSync, readFileSync } from 'node:fs';
25
25
  import { join } from 'node:path';
26
26
  import { HOOK_EVENT_NAMES, HookConfigSchema } from '@reticlehq/core/hooks';
27
27
  import { ReticleDir } from '@reticlehq/core';
@@ -65,18 +65,29 @@ let cached;
65
65
  */
66
66
  export function readHookConfig(reticleRoot) {
67
67
  const path = join(reticleRoot, ReticleDir.HOOKS_FILE);
68
- let mtime;
68
+ // Open once, then stat and read the SAME descriptor: the mtime the cache keys on is the mtime of
69
+ // the bytes read, not of whatever the path pointed at a moment earlier.
70
+ let fd;
69
71
  try {
70
- mtime = statSync(path).mtimeMs;
72
+ fd = openSync(path, 'r');
71
73
  }
72
74
  catch {
73
75
  cached = undefined;
74
76
  return {};
75
77
  }
78
+ try {
79
+ return readOpenHookConfig(fd);
80
+ }
81
+ finally {
82
+ closeSync(fd);
83
+ }
84
+ }
85
+ function readOpenHookConfig(fd) {
86
+ const mtime = fstatSync(fd).mtimeMs;
76
87
  if (cached !== undefined && cached.at === mtime)
77
88
  return cached.config;
78
89
  try {
79
- const parsed = HookConfigSchema.safeParse(JSON.parse(readFileSync(path, 'utf8')));
90
+ const parsed = HookConfigSchema.safeParse(JSON.parse(readFileSync(fd, 'utf8')));
80
91
  if (!parsed.success) {
81
92
  reportOnce(`config:${String(mtime)}`, `${ReticleDir.HOOKS_FILE} is not a map of event→command`);
82
93
  cached = { at: mtime, config: {} };
@@ -0,0 +1,9 @@
1
+ export declare function toolLogPath(): string | undefined;
2
+ export declare function logToolCall(path: string, entry: {
3
+ tool: string;
4
+ args: unknown;
5
+ at: number;
6
+ ms: number;
7
+ result?: unknown;
8
+ error?: string;
9
+ }): void;
@@ -0,0 +1,29 @@
1
+ import { appendFileSync } from 'node:fs';
2
+ import { currentDrivenBy } from './driven-by.js';
3
+ /**
4
+ * Every tool call, its arguments and what it answered, appended to a file a person names.
5
+ *
6
+ * Off unless `RETICLE_TOOL_LOG` is set. It exists to put two drives side by side (an agent's and
7
+ * the Harness's) and read what each one asked and was told; `drivenBy` says which was which. Local
8
+ * only: nothing here leaves the machine.
9
+ */
10
+ const TOOL_LOG_ENV = 'RETICLE_TOOL_LOG';
11
+ /** A page snapshot can be large; past this a record keeps its head and says it was cut. */
12
+ const MAX_RESULT_CHARS = 200_000;
13
+ export function toolLogPath() {
14
+ const path = process.env[TOOL_LOG_ENV];
15
+ return path === undefined || 0 === path.trim().length ? undefined : path;
16
+ }
17
+ export function logToolCall(path, entry) {
18
+ try {
19
+ const text = JSON.stringify(entry.result ?? null);
20
+ const result = text.length > MAX_RESULT_CHARS
21
+ ? { truncated: text.slice(0, MAX_RESULT_CHARS) }
22
+ : entry.result;
23
+ const drivenBy = currentDrivenBy();
24
+ appendFileSync(path, `${JSON.stringify({ ...entry, result, ...(drivenBy === undefined ? {} : { drivenBy }) })}\n`);
25
+ }
26
+ catch {
27
+ /* a log that cannot be written never fails the call it describes */
28
+ }
29
+ }
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { StartOptions } from './start-options.js';
2
2
  export type { StartOptions } from './start-options.js';
3
3
  import { Bridge } from './portal/bridge/bridge.js';
4
4
  import type { RealInputProvider } from './portal/input/real-input.js';
5
+ export { replayVerdictLine } from './language/flows/replay-from-hud.js';
5
6
  export { ReticleTool } from '@reticlehq/core';
6
7
  export { RingBuffer } from '@reticlehq/engine/window/ring-buffer.js';
7
8
  export { Bridge } from './portal/bridge/bridge.js';
package/dist/index.js CHANGED
@@ -1,20 +1,23 @@
1
- import { writeHarnessSwitch } from './memory/cloud/harness-switch.js';
2
- import { harnessConfigSource } from './memory/cloud/harness-config.js';
1
+ import { applyHarnessSwitch, harnessConfigsByRoot } from './memory/cloud/harness-config.js';
2
+ import { coveragePercents } from './features/exhaust/ledger.js';
3
+ import { firstRunWiring } from './portal/session/first-run-wiring.js';
3
4
  import { fetchPlatformConfig } from './features/harness/platform-config.js';
5
+ import { watchAccountFiles } from './memory/impact/account-watch.js';
6
+ import { SESSION_FILE } from './command/cli/cloud-kit.js';
4
7
  import { join } from 'node:path';
5
- import { existsSync, readFileSync } from 'node:fs';
6
8
  import { homedir } from 'node:os';
7
- import { linkedCloudPort, platformEnvPort } from './memory/cloud/cloud-config.js';
9
+ import { linkedCloudPort, platformEnvPort, sessionCloudPort } from './memory/cloud/cloud-config.js';
10
+ import { sessionRoot } from './memory/project/session-root.js';
8
11
  import { attachCloudSync } from './memory/cloud/sync-daemon.js';
9
12
  import { wireHooks } from './hooks/hook-commands.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, projectDirectoryFor, } from './memory/project/artifact-root-resolver.js';
13
+ import { currentDrivenBy } from './hooks/driven-by.js';
14
+ import { artifactRootResolver, knownProjectCandidates, projectDirectoryFor, } from './memory/project/artifact-root-resolver.js';
15
+ import { servingDirectoryOf } from './portal/session/serving-directory.js';
13
16
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
- import { AGENT_STOPPED_NOTICE, RETICLE_DEFAULT_PORT, bridgeWsUrl, ReticleCommand, ReticleDir, ReticleEnv, LOOPBACK_HOST, ReplayStatus, EventType, } from '@reticlehq/core';
17
+ import { AGENT_STOPPED_NOTICE, RETICLE_DEFAULT_PORT, bridgeWsUrl, ReticleCommand, ReticleDir, ReticleEnv, LOOPBACK_HOST, EventType, } from '@reticlehq/core';
15
18
  import { originOf } from './portal/session/session-manager.js';
16
19
  import { setBrowserMode, BrowserMode } from './telemetry/browser-mode.js';
17
- import { replayAndLearn } from './language/flows/flow-learning.js';
20
+ import { replayFromHud } from './language/flows/replay-from-hud.js';
18
21
  import { createSharedServer } from './surface/http-server.js';
19
22
  import { openLoopbackAlias } from './command/daemon/binding/loopback-alias.js';
20
23
  import { reportAppInstrumented } from './telemetry/app-instrumented.js';
@@ -25,7 +28,7 @@ import { connectionSkew } from './command/version/version-nudge.js';
25
28
  import { SERVER_VERSION } from './command/version/identity/server-version.js';
26
29
  import { BaselineStore } from './memory/project/baselines.js';
27
30
  import { RecordingStore } from './language/flows/recording/tape/recordings.js';
28
- import { initImpact } from './memory/impact/impact-recorder.js';
31
+ import { initImpact, impactSnapshot } from './memory/impact/impact-recorder.js';
29
32
  import { flowAuthor } from './language/flows/flow-author.js';
30
33
  import { FlowStore } from './language/flows/flows.js';
31
34
  import { buildFlowChips } from './language/flows/flow-scope.js';
@@ -40,7 +43,9 @@ import { createRunnerPort } from './judgement/runs/runner-port.js';
40
43
  import { RunStore } from './judgement/runs/artifact/run-store.js';
41
44
  import { startVerifyServer } from './judgement/runs/verify-server.js';
42
45
  import { createMcpServer } from './surface/mcp/mcp.js';
46
+ import { instructionStateAt } from './surface/mcp/mcp-proxy.js';
43
47
  import { LEASE_ACQUIRE_TOOL } from './surface/tools/lease-tools.js';
48
+ import { startChatDrives } from './surface/tools/chat-drives.js';
44
49
  import { runTool } from './surface/tools/invoke-tool.js';
45
50
  import { SessionReaper, endAllSessions, MCP_DISCONNECT_SUMMARY, } from './portal/session/session-reaper.js';
46
51
  import { wireSessionScope } from './portal/session/no-session-watch.js';
@@ -53,9 +58,7 @@ import { BrowserPool } from './portal/pool/browser-pool.js';
53
58
  import { AGENT_ALREADY_DRIVING_ELSEWHERE, shouldGreetWithLeaseNotice, } from './portal/session/lease-visibility.js';
54
59
  import { playwrightLauncher, resolveMaxContexts } from './portal/pool/playwright-launcher.js';
55
60
  import { LeaseReaper } from './portal/pool/lease-reaper.js';
56
- import { findProjectConfig, projectIdsAt, readJournalEnabled, readProjectId, } from './command/cli/ports/resolve/cli-port.js';
57
- import { hasAnyProjectConnectedBefore } from './memory/recall/prior/connection-memory.js';
58
- import { reticleStateHome } from './command/daemon/daemon.js';
61
+ import { findProjectConfig, readJournalEnabled, readProjectId, } from './command/cli/ports/resolve/cli-port.js';
59
62
  import { probeLaunchableChromium } from './launch-chromium.js';
60
63
  import { attachJournal } from './wire-journal.js';
61
64
  import { reportOnboardingStep } from './telemetry/onboarding-funnel.js';
@@ -63,14 +66,7 @@ import { AMBIENT_RECORDING } from './language/flows/recording/tape/recordings.js
63
66
  import { readRetainPolicy } from './memory/journal/on-disk/retain-policy.js';
64
67
  import { log } from './log.js';
65
68
  import { readReaderBundle, zeroInstallScript as zeroInstallScriptFor, } from './portal/pool/zero-install.js';
66
- /** A human-facing one-liner for a panel replay verdict — ✓ passed / ⚠ drifted / ✗ errored. */
67
- function replayVerdictLine(result) {
68
- if (result.status === ReplayStatus.OK)
69
- return `✓ "${result.name}" passed`;
70
- if (result.status === ReplayStatus.DRIFT)
71
- return `⚠ "${result.name}" drifted — a step no longer matches`;
72
- return `✗ "${result.name}" failed — ${result.error?.message ?? 'could not replay'}`;
73
- }
69
+ export { replayVerdictLine } from './language/flows/replay-from-hud.js';
74
70
  // Re-exported from the contract, where the names now live: a consumer importing the server should
75
71
  // not have to know that the vocabulary moved.
76
72
  export { ReticleTool } from '@reticlehq/core';
@@ -138,6 +134,7 @@ function createBrowserPool(headless, reader) {
138
134
  maxContexts,
139
135
  genSessionId,
140
136
  zeroInstallScript,
137
+ launchHeaded: playwrightLauncher({ headless: false }),
141
138
  });
142
139
  }
143
140
  /**
@@ -253,40 +250,25 @@ function sdkFixForProject(projectId) {
253
250
  * not. Deciding that here would mean reading every cloud.json on every tick.
254
251
  */
255
252
  function knownProjectRoots() {
256
- const roots = new Set();
257
- try {
258
- const path = join(homedir(), ReticleDir.ROOT, PROJECT_REGISTRY_FILE);
259
- if (existsSync(path)) {
260
- const registry = parseProjectRegistry(JSON.parse(readFileSync(path, 'utf8')));
261
- for (const candidate of projectCandidates(registry))
262
- roots.add(join(candidate.directory, ReticleDir.ROOT));
263
- }
264
- }
265
- catch {
266
- // A registry that cannot be read is an empty one — never a reason to stop syncing.
267
- }
268
- try {
269
- for (const config of discoverProjectConfigs(process.cwd()).found)
270
- roots.add(join(config.directory, ReticleDir.ROOT));
271
- }
272
- catch {
273
- // Same: a diagnostic walk that throws must not take the sync loop with it.
274
- }
275
- return [...roots];
253
+ return [...new Set(knownProjectCandidates().map((c) => join(c.directory, ReticleDir.ROOT)))];
276
254
  }
277
255
  /** This project's platform credential (stored, else the env key), as the env the platform readers take. */
278
256
  const platformEnvFor = (root) => platformEnvPort(createNodeFileSystem(), root ?? join(process.cwd(), ReticleDir.ROOT), homedir(), process.env);
257
+ const loadHarnessConfig = async (root) => fetchPlatformConfig(await platformEnvFor(root)());
279
258
  export async function start(options = {}) {
280
259
  const port = options.port ?? RETICLE_DEFAULT_PORT;
260
+ const configForRoot = harnessConfigsByRoot(loadHarnessConfig);
281
261
  // Open the user's impact record before anything can connect. Not inside the MCP branch: a daemon
282
262
  // serving a browser with no agent attached still has a HUD to answer, and a report that reads
283
263
  // "nothing recorded yet" over a month of history on disk is the worst version of this feature.
284
264
  initImpact({
265
+ sdkVersion: SERVER_VERSION,
285
266
  reticleRoot: options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT),
286
267
  // The daemon owns both sides of this seam, so it is the layer that may join them: the cache
287
268
  // lives in cloud memory, the platform read lives in the harness feature, and neither is allowed
288
269
  // to reach for the other.
289
- config: harnessConfigSource(async () => fetchPlatformConfig(await platformEnvFor(options.reticleRoot)())),
270
+ configForRoot,
271
+ coverageForRoot: (root) => () => coveragePercents(root),
290
272
  });
291
273
  const uninstallHooks = wireHooks(options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT), readProjectId(process.cwd()));
292
274
  const security = await resolveBridgeSecurityWithAutoToken(options);
@@ -302,7 +284,7 @@ export async function start(options = {}) {
302
284
  bridge.sessions.setDefaultScope({ projectId: activeProjectId });
303
285
  }
304
286
  const baselines = new BaselineStore();
305
- const recordings = new RecordingStore();
287
+ const recordings = new RecordingStore(() => currentDrivenBy() !== undefined);
306
288
  // drive precedence: driveUrl (launch+own a browser) → CDP (attach) → none.
307
289
  let pool;
308
290
  let leaseReaper;
@@ -310,6 +292,7 @@ export async function start(options = {}) {
310
292
  // page and the SDK session share an origin, so a NET_DETAIL is pushed to the matching connected session.
311
293
  const routeNetworkDetail = makeNetworkDetailRouter(bridge, options.driveUrl);
312
294
  const { realInput, owned } = await resolveRealInput(options, () => bridge.close(), routeNetworkDetail);
295
+ let chatDrives;
313
296
  if (options.mcp !== false) {
314
297
  // cwd/Date.now are confined to start — never inside reticle-dir.ts's pure logic (rule 7).
315
298
  const fs = createNodeFileSystem();
@@ -367,7 +350,7 @@ export async function start(options = {}) {
367
350
  fs,
368
351
  reticleRoot,
369
352
  linkedCloud: linkedCloudPort(fs, reticleRoot, homedir(), process.env),
370
- artifactRootFor: artifactRootResolver(reticleRoot),
353
+ artifactRootFor: artifactRootResolver(reticleRoot, { servingDirectory: servingDirectoryOf }),
371
354
  now,
372
355
  bridgePort: port,
373
356
  browserProbe: probeLaunchableChromium,
@@ -376,13 +359,13 @@ export async function start(options = {}) {
376
359
  ...(activeProjectId === undefined ? {} : { projectId: activeProjectId }),
377
360
  };
378
361
  const profile = resolveToolSurface(options.toolProfile);
379
- const server = createMcpServer(realInput !== undefined ? { ...deps, realInput } : deps, profile,
380
- // A session live RIGHT NOW is stronger evidence than the durable memory, which can be empty or
381
- // stale for a project the plugin wired without writing `.reticle.json` (see connection-memory.ts's
382
- // KNOWN LIMIT). Without this OR, that project's `initialize` led with the first-install steps
383
- // while a real session was already connected — see #1138.
384
- bridge.sessions.count() > 0 ||
385
- hasAnyProjectConnectedBefore(reticleStateHome(), port, projectIdsAt(process.cwd())));
362
+ const toolDeps = realInput !== undefined ? { ...deps, realInput } : deps;
363
+ // The platform chat sees this app too: `reticle drive` in a window runs here, not in a daemon.
364
+ chatDrives = startChatDrives(toolDeps, bridge.sessions, fs, {
365
+ sessionCloud: sessionCloudPort(fs, homedir(), process.env, (id) => sessionRoot(toolDeps, id)),
366
+ version: SERVER_VERSION,
367
+ }, log);
368
+ const server = createMcpServer(toolDeps, profile, instructionStateAt(port, bridge.sessions.count()));
386
369
  // When the agent (the MCP client) disconnects cleanly, end every active session at once so the
387
370
  // HUD doesn't linger. (If the agent instead KILLS this process, the WS dies and the browser
388
371
  // self-ends via SESSION_LIFECYCLE.BRIDGE_LOST_MS — see transport.ts.)
@@ -401,6 +384,7 @@ export async function start(options = {}) {
401
384
  uninstallHooks();
402
385
  await cleanupCaptureDirectories();
403
386
  leaseReaper?.stop();
387
+ chatDrives?.stop();
404
388
  await pool?.shutdown();
405
389
  await owned?.dispose();
406
390
  await bridge.close();
@@ -415,17 +399,21 @@ export async function start(options = {}) {
415
399
  */
416
400
  export async function startDaemon(options = {}) {
417
401
  const port = options.port ?? RETICLE_DEFAULT_PORT;
402
+ let pushHarnessConfig = () => undefined;
403
+ const configForRoot = harnessConfigsByRoot(loadHarnessConfig, (root) => pushHarnessConfig(root));
418
404
  // The SAME line as in `start`, because these are two entry points that each wire their own world
419
405
  // and the daemon is the one that actually serves people. Wired only in `start`, the impact record
420
406
  // was never opened in the process the HUD talks to: tool calls still recorded (the dispatch
421
407
  // chokepoint opens it lazily), but a tab that connected before the first tool call was pushed
422
408
  // nothing, so the report read "nothing recorded yet" over a file with history in it.
423
409
  initImpact({
410
+ sdkVersion: SERVER_VERSION,
424
411
  reticleRoot: options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT),
425
412
  // The daemon owns both sides of this seam, so it is the layer that may join them: the cache
426
413
  // lives in cloud memory, the platform read lives in the harness feature, and neither is allowed
427
414
  // to reach for the other.
428
- config: harnessConfigSource(async () => fetchPlatformConfig(await platformEnvFor(options.reticleRoot)())),
415
+ configForRoot,
416
+ coverageForRoot: (root) => () => coveragePercents(root),
429
417
  });
430
418
  const security = await resolveBridgeSecurityWithAutoToken(options);
431
419
  const shared = createSharedServer(security.token === undefined ? {} : { token: security.token });
@@ -435,6 +423,14 @@ export async function startDaemon(options = {}) {
435
423
  sdkFix: sdkFixForProject,
436
424
  ...security,
437
425
  });
426
+ const repaint = (only) => {
427
+ for (const session of bridge.sessions.all()) {
428
+ const root = session.artifactRoot ?? options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT);
429
+ if (only === undefined || only === root)
430
+ session.pushImpact(() => impactSnapshot(root), true);
431
+ }
432
+ };
433
+ pushHarnessConfig = repaint;
438
434
  // The daemon owns listen (below), so the real bind error is reported there; absorb bridge.ready's
439
435
  // mirror rejection so a port collision can't surface as an unhandled promise rejection.
440
436
  void bridge.ready.catch(() => undefined);
@@ -476,7 +472,7 @@ export async function startDaemon(options = {}) {
476
472
  const flows = new FlowStore(fs, reticleRoot, { now });
477
473
  // Built here rather than inside `deps` below, so teardown can save what a drive recorded. Both
478
474
  // paths pass the same pair — `daemon-parity.test.ts` is what keeps them from drifting apart.
479
- const recordings = new RecordingStore();
475
+ const recordings = new RecordingStore(() => currentDrivenBy() !== undefined);
480
476
  /*
481
477
  * Bound AFTER the sync daemon exists, read only when a run is actually written.
482
478
  *
@@ -529,11 +525,17 @@ export async function startDaemon(options = {}) {
529
525
  // `syncNow`, not `nudge`: a nudge schedules a cycle soon, which is right for "a run landed" and
530
526
  // wrong for a button somebody is watching. Never awaited.
531
527
  bridge.attachSyncRequest(() => void cloudSync.syncNow());
532
- // The panel's harness switch. Written through to the platform rather than kept locally, so the
533
- // console and the panel cannot disagree about a setting they both offer. Nothing is awaited and a
534
- // failure is not surfaced: the next snapshot re-reads the platform, so a lost write shows up as
535
- // the switch springing back, which is the truthful outcome.
536
- bridge.attachHarnessRequest((enabled) => void platformEnvFor(reticleRoot)().then((env) => writeHarnessSwitch(env, enabled)));
528
+ // The panel's harness switch, written through to the platform so console and panel cannot disagree.
529
+ if (options.hudSignIn !== undefined)
530
+ bridge.attachSigninRequest(options.hudSignIn(() => repaint()));
531
+ // logout and link run in another process: repaint when their files change. See account-watch.ts.
532
+ const accountFiles = watchAccountFiles(() => repaint());
533
+ accountFiles.watch(join(homedir(), ReticleDir.ROOT, SESSION_FILE));
534
+ bridge.attachSessionReady((s) => accountFiles.watch(join(s.artifactRoot ?? reticleRoot, ReticleDir.CLOUD_LINK_FILE)));
535
+ bridge.attachHarnessRequest((on, s) => {
536
+ const root = s.artifactRoot ?? reticleRoot;
537
+ applyHarnessSwitch(configForRoot, root, on, platformEnvFor(root));
538
+ });
537
539
  // Scope auto-selection to the active project (from .reticle.json) so a stray tab from another app is
538
540
  // never picked when the agent omits a sessionId. Explicit per-call scope/sessionId still overrides.
539
541
  // Scope + the no-session diagnosis: "no browser session connected" is the error that ends most
@@ -562,12 +564,14 @@ export async function startDaemon(options = {}) {
562
564
  // daemon — the common case — so flows were listed, loaded and healed against wherever the
563
565
  // daemon happened to be launched. Wired in only one of the two places, a resolver is a resolver
564
566
  // that does not run.
565
- artifactRootFor: artifactRootResolver(reticleRoot),
567
+ artifactRootFor: artifactRootResolver(reticleRoot, { servingDirectory: servingDirectoryOf }),
566
568
  now,
567
569
  bridgePort: port,
568
570
  browserProbe: probeLaunchableChromium,
571
+ firstRun: firstRunWiring({ port, cliPath: process.argv[1] ?? '' }),
569
572
  // A finished verification should not sit behind a one-minute timer — see ToolDeps.onRunPersisted.
570
573
  onRunPersisted: () => cloudSync.nudge(),
574
+ syncNow: () => cloudSync.syncNow(),
571
575
  };
572
576
  const profile = resolveToolSurface(options.toolProfile);
573
577
  const effectiveDeps = realInput !== undefined ? { ...deps, realInput } : deps;
@@ -575,15 +579,14 @@ export async function startDaemon(options = {}) {
575
579
  // being told to wire itself on the next agent that connects.
576
580
  shared.attachMcp((peerSkew) => createMcpServer(peerSkew === undefined
577
581
  ? effectiveDeps
578
- : { ...effectiveDeps, peerSkew: connectionSkew(peerSkew) }, profile,
579
- // See the sibling call in `start`: a live session outweighs empty/stale durable memory (#1138).
580
- bridge.sessions.count() > 0 ||
581
- hasAnyProjectConnectedBefore(reticleStateHome(), port, projectIdsAt(process.cwd()))));
582
- // `reticle drive <url>` when this daemon already owns the port: it asks HERE instead of trying to
583
- // bind a port we are holding, and gets the same pooled context an agent's reticle_lease returns —
584
- // through runTool, so it is counted and reported like any other call rather than being a second,
585
- // invisible dispatch path. See cli/drive/drive-attach.ts for why attaching beats refereeing the race.
582
+ : { ...effectiveDeps, peerSkew: connectionSkew(peerSkew) }, profile, instructionStateAt(port, bridge.sessions.count())));
583
+ // `reticle drive <url>` against this daemon's port, through runTool. See cli/drive/drive-attach.ts.
586
584
  shared.attachDrive((url) => runTool(LEASE_ACQUIRE_TOOL, effectiveDeps, { url }));
585
+ // The platform chat's apps and drives, run here. See chat-drives.ts.
586
+ const chatDrives = startChatDrives(effectiveDeps, bridge.sessions, fs, {
587
+ sessionCloud: sessionCloudPort(fs, homedir(), process.env, (id) => sessionRoot(effectiveDeps, id)),
588
+ version: SERVER_VERSION,
589
+ }, log);
587
590
  // Optional OEM/CI verify endpoint: a host platform POSTs to /verify and gets an ReticleVerificationRun,
588
591
  // driving the same flow-replay machinery the agent uses — no MCP stdio, no human. Each verdict is
589
592
  // persisted via RunStore. Localhost-bound + token-guarded. Off unless `reticle serve --http`.
@@ -601,16 +604,9 @@ export async function startDaemon(options = {}) {
601
604
  // verdict into the same activity log they watch the agent in. The page animates via the normal
602
605
  // replay path, so they see it re-drive and the ✓/⚠/✗ land.
603
606
  bridge.attachReplay((sessionId, flowName) => {
604
- const session = bridge.sessions.get(sessionId);
605
- if (session === undefined)
607
+ if (bridge.sessions.get(sessionId) === undefined)
606
608
  return;
607
- session.pushNarration(`▶ Replaying "${flowName}"…`);
608
- replayAndLearn(effectiveDeps, { flowName, sessionId })
609
- .then((result) => session.pushNarration(replayVerdictLine(result)))
610
- .catch((error) => {
611
- const message = error instanceof Error ? error.message : String(error);
612
- session.pushNarration(`✗ Replay "${flowName}" failed — ${message}`);
613
- });
609
+ void replayFromHud(effectiveDeps, (id) => bridge.sessions.get(id), sessionId, flowName);
614
610
  });
615
611
  // On connect, hand the panel the replayable flows so it can render the ▶ list. Scoped to the
616
612
  // connecting session's project (a shared daemon serves many apps; each panel shows only its own
@@ -680,6 +676,8 @@ export async function startDaemon(options = {}) {
680
676
  if (vh !== undefined)
681
677
  await new Promise((resolve) => vh.server.close(() => resolve()));
682
678
  leaseReaper.stop();
679
+ chatDrives.stop();
680
+ accountFiles.close();
683
681
  await cloudSync.flush(); // not stop(): the last run written is the one nobody has yet
684
682
  await loopbackAlias.close?.();
685
683
  await pool.shutdown();
@@ -689,12 +687,8 @@ export async function startDaemon(options = {}) {
689
687
  },
690
688
  };
691
689
  }
692
- // The OpenVerification binding, exported so a conformance run can be driven from outside this package.
693
- //
694
- // It was written and then not reachable: `conformanceClient` takes a `WebRealm`, a `WebRealm`
695
- // takes a live `Session`, and a `Session` exists only inside a running daemon. A runner that
696
- // cannot import it cannot score anything, which would have made the whole conformance chain
697
- // complete and unusable.
690
+ // The OpenVerification binding, exported so a conformance run can be driven from outside this
691
+ // package: a `WebRealm` needs a live `Session`, which exists only inside a running daemon.
698
692
  export { WebRealm } from './portal/realm/web-realm.js';
699
693
  export { conformanceClient } from './portal/realm/conformance-client.js';
700
694
  /**
@@ -23,6 +23,7 @@ export function computeVerdict(input) {
23
23
  .length + input.checks.filter((c) => c.status === Verified.YES).length;
24
24
  const fails = input.flows.filter((f) => f.status === RunFlowStatus.FAIL).length +
25
25
  input.checks.filter((c) => c.status === Verified.NO).length;
26
+ const skippedFlows = input.flows.filter((f) => f.status === RunFlowStatus.SKIPPED).length;
26
27
  const blockingRisks = input.risks.filter((r) => r.gated).length;
27
28
  const reasons = [];
28
29
  for (const f of input.flows) {
@@ -31,6 +32,9 @@ export function computeVerdict(input) {
31
32
  ? `flow ${f.name}: ${f.failureReason}`
32
33
  : `flow ${f.name} failed`);
33
34
  }
35
+ else if (f.status === RunFlowStatus.SKIPPED && f.failureReason !== undefined) {
36
+ reasons.push(`flow ${f.name}: ${f.failureReason}`);
37
+ }
34
38
  }
35
39
  for (const c of input.checks) {
36
40
  if (c.status === Verified.NO)
@@ -47,6 +51,8 @@ export function computeVerdict(input) {
47
51
  status = VerdictStatus.PARTIAL;
48
52
  else if (fails > 0)
49
53
  status = VerdictStatus.FAIL;
54
+ else if (skippedFlows > 0 && passes > 0)
55
+ status = VerdictStatus.PARTIAL;
50
56
  else if (0 === passes) {
51
57
  // Nothing passed and nothing failed, so nothing was proved -- an empty run, or one whose every
52
58
  // flow was skipped. Reporting that as PASS is the false green this whole file exists to prevent:
@@ -5,7 +5,7 @@
5
5
  * "why" is lifted from the replay's decision envelope (or its error) into failureReason.
6
6
  */
7
7
  import { ReplayStatus, RunFlowStatus, type FlowFile, type FlowReplayResult, type RunFlowResult } from '@reticlehq/core';
8
- /** OK → PASS; DRIFT/ERROR → FAIL (a healed flow is produced by the heal path, not plain replay). */
8
+ /** OK → PASS; UNVERIFIABLE → SKIPPED; DRIFT/ERROR → FAIL (a healed flow is produced by the heal path, not plain replay). */
9
9
  export declare function runFlowStatusOf(status: ReplayStatus): RunFlowStatus;
10
10
  /** Map one replay (plus its measured duration) into a RunFlowResult for the artifact's flows[]. */
11
11
  export declare function mapReplayToFlowResult(replay: FlowReplayResult, durationMs: number, flow?: FlowFile): RunFlowResult;
@@ -6,9 +6,13 @@
6
6
  */
7
7
  import { ReplayStatus, RUN_RECORDING_MAX_BYTES, RUN_STEP_RESULTS_MAX, RUN_TEXT_MAX, RunFlowStatus, } from '@reticlehq/core';
8
8
  import { SUCCESS_STEP_TOOL } from '../../../language/flows/flow-success.js';
9
- /** OK → PASS; DRIFT/ERROR → FAIL (a healed flow is produced by the heal path, not plain replay). */
9
+ /** OK → PASS; UNVERIFIABLE → SKIPPED; DRIFT/ERROR → FAIL (a healed flow is produced by the heal path, not plain replay). */
10
10
  export function runFlowStatusOf(status) {
11
- return status === ReplayStatus.OK ? RunFlowStatus.PASS : RunFlowStatus.FAIL;
11
+ if (status === ReplayStatus.OK)
12
+ return RunFlowStatus.PASS;
13
+ if (status === ReplayStatus.UNVERIFIABLE)
14
+ return RunFlowStatus.SKIPPED;
15
+ return RunFlowStatus.FAIL;
12
16
  }
13
17
  const clip = (text) => text.length > RUN_TEXT_MAX ? `${text.slice(0, RUN_TEXT_MAX - 1)}…` : text;
14
18
  /** One step as synced: what it did, where, and whether it held. */
@@ -39,9 +43,12 @@ function recordingOf(flow) {
39
43
  }
40
44
  /** Map one replay (plus its measured duration) into a RunFlowResult for the artifact's flows[]. */
41
45
  export function mapReplayToFlowResult(replay, durationMs, flow) {
42
- // A replay that could not start returns OK with no steps. Nothing ran, so it is SKIPPED — which
43
- // every reader of a run (the verdict, the gate) already refuses to count as a pass.
44
- const status = replay.unverifiable === undefined ? runFlowStatusOf(replay.status) : RunFlowStatus.SKIPPED;
46
+ // A replay that could not start or complete returns UNVERIFIABLE (or carries an unverifiable caveat).
47
+ // Nothing was verified, so it is SKIPPED — which every reader of a run (the verdict, the gate)
48
+ // refuses to count as a pass.
49
+ const status = replay.status === ReplayStatus.UNVERIFIABLE || replay.unverifiable !== undefined
50
+ ? RunFlowStatus.SKIPPED
51
+ : runFlowStatusOf(replay.status);
45
52
  const failureReason = replay.unverifiable !== undefined
46
53
  ? replay.unverifiable.reason
47
54
  : status === RunFlowStatus.FAIL
@@ -49,13 +56,16 @@ export function mapReplayToFlowResult(replay, durationMs, flow) {
49
56
  replay.decision?.summary ??
50
57
  replay.error?.message ??
51
58
  'flow failed')
52
- : undefined;
59
+ : status === RunFlowStatus.SKIPPED
60
+ ? 'flow could not be graded'
61
+ : undefined;
53
62
  // A flow with a success oracle gets a synthetic 'success' step appended by replay; surface its label
54
63
  // as the run's `oracle` so the verdict counts this flow as consequence-backed (→ HIGH confidence),
55
64
  // not a bare smoke click. Without this, an oracle-backed pass reads as MEDIUM and undersells itself.
56
65
  const oracle = replay.steps.find((s) => s.tool === SUCCESS_STEP_TOOL)?.anchor;
57
66
  return {
58
67
  name: replay.name,
68
+ ...(flow?.template === undefined ? {} : { template: flow.template }),
59
69
  status,
60
70
  steps: replay.steps.length,
61
71
  durationMs,
@@ -38,7 +38,7 @@ export declare class RunStore {
38
38
  * Guards runId as a safe path segment BEFORE building the path — a runId can originate from a caller
39
39
  * (an OEM may set it), so an unsafe value must never escape .reticle/runs/ (mirrors the read guard).
40
40
  */
41
- write(run: ReticleVerificationRun): Promise<void>;
41
+ write(given: ReticleVerificationRun): Promise<void>;
42
42
  /** Never throws. Invalid id / missing → MISSING; bad JSON or failed schema → MALFORMED. */
43
43
  read(runId: RunId): Promise<ReadRunResult>;
44
44
  /** List run ids on disk (filenames minus.json). Empty when the runs dir is absent. */