@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
@@ -0,0 +1,342 @@
1
+ /**
2
+ * The Harness with no journey named: plan the drive from what the project knows, then run the plan.
3
+ *
4
+ * Saved flows replay in lanes side by side (each lane its own leased browser context, when the pool
5
+ * can lease one), flows that start the same way are driven once and branched from, and the open
6
+ * journeys (personas, the user's request, unproved intents) go to the model. The plan is drawn on
7
+ * the HUD and redrawn as each part starts and ends.
8
+ */
9
+ import { randomUUID } from 'node:crypto';
10
+ import { join } from 'node:path';
11
+ import { ReticleCommand, ReticleTool, ScriptStatus, asRecord } from '@reticlehq/core';
12
+ import { PromptContextSchema, checkScript } from '@reticlehq/core/artifacts';
13
+ import { runScript, } from '../../features/harness/script/script-run.js';
14
+ import { CUSTOM_DRIVER_NAME } from '../../features/harness/drivers.js';
15
+ import { checkTally, verdictLine } from '../../features/harness/drive-report.js';
16
+ import { runHarness, StopReason, } from '../../features/harness/harness.js';
17
+ import { reticleDirPaths } from '../../memory/project/dir/reticle-dir.js';
18
+ import { harnessRunId, noteHarnessGoal } from '../../judgement/runs/drive-run.js';
19
+ import { openSessionIntents } from '../../memory/intent/open-intents.js';
20
+ import { sessionRoot, sessionTarget } from '../../memory/project/session-root.js';
21
+ import { leasableAppUrl } from '../../language/flows/flow-tools.js';
22
+ import { acquireLeasedSession } from './lease-tools.js';
23
+ import { reticleToolset } from './harness-toolset.js';
24
+ import { laneIds } from '../../features/harness/script/lane-ids.js';
25
+ import { appControlsOf } from '../../features/harness/script/app-controls.js';
26
+ import { PlanStepKind, aboutTheApp, planAsText, withoutReplays, } from './harness-plan.js';
27
+ import { journeyResults, personasIn, proposeScript, reportPlanResults, } from '../../features/harness/platform/script.js';
28
+ import { checkGoals, goalsIn } from '../../features/harness/goals.js';
29
+ import { serverOptionsFromEnv } from '../../features/harness/platform/server-driver.js';
30
+ import { MSG_HARNESS_DISABLED, bankOpenRecording, buildDriver, flowsThatCheckNothing, maxStepsFromEnv, narrator, pinned, productRules, readFlows, readPlan, reconcileFlows, recordPersona, refusedByPlatform, withProjectFlows, } from './harness-explore.js';
31
+ /** A request older than this is about some earlier task, not the one being driven. */
32
+ const REQUEST_FRESH_MS = 6 * 60 * 60 * 1000;
33
+ const MAX_GOAL_CHARS = 500;
34
+ /**
35
+ * Lanes at once. Each is a fresh context loading the app cold, and eight against a dev server timed
36
+ * out together. ponytail: a fixed cap; read it from the pool's measured load time if 4 is too few.
37
+ */
38
+ const MAX_LANES = 4;
39
+ /** How often a scripted drive asks whether autonomous driving was switched off. */
40
+ const STOP_POLL_MS = 10_000;
41
+ /**
42
+ * The platform plans the drive; this machine runs the plan: saved flows replayed in lanes side by side, shared starts
43
+ * driven once and branched from, open journeys handed to the model. `undefined` when there is
44
+ * nothing to plan from, so the caller drives the app with no plan, as before.
45
+ */
46
+ export async function exploreScript(deps, env, options, before) {
47
+ const reads = withProjectFlows(deps, options.sessionId);
48
+ const plan = await readPlan(reads, options.sessionId);
49
+ // A named journey is the whole plan: no other flows replayed, no personas proposed beside it.
50
+ const focus = options.focus;
51
+ const replay = focus === undefined
52
+ ? plan.steps.filter((s) => PlanStepKind.REPLAY === s.kind).map((s) => s.target)
53
+ : [];
54
+ const goals = focus === undefined ? await memoryGoals(deps, options.sessionId) : [focus];
55
+ const gaps = plan.steps.filter((s) => PlanStepKind.DRIVE === s.kind).map((s) => s.why);
56
+ const flows = await readFlows(reads);
57
+ const known = await productRules(deps, options.sessionId);
58
+ // The platform plans: personas, order, branches and the product rules each journey proves.
59
+ // A platform that cannot plan leaves the caller to drive the journey through it unplanned.
60
+ const platform = serverOptionsFromEnv(env);
61
+ if (platform === undefined)
62
+ return undefined;
63
+ // What the screen offers, for a named journey too. The platform splits a broad request ("test my
64
+ // app, make a plan") into journeys through these controls and leaves a narrow one whole; without
65
+ // them, a chat-requested drive of a desktop app planned one step that restated the request.
66
+ const about = await aboutTheApp(planAsText(plan), (name, args) => reticleToolset(deps, pinned(options)).invoke(name, args), await controlsSeenBefore(deps, options.sessionId));
67
+ const personas = focus === undefined ? personasIn(flows) : [];
68
+ const proposed = await proposeScript(platform, {
69
+ about,
70
+ flows,
71
+ replay,
72
+ goals,
73
+ gaps,
74
+ rules: known,
75
+ personas,
76
+ });
77
+ if (proposed === undefined || 0 < checkScript(proposed.script).length)
78
+ return undefined;
79
+ const { script, planId } = proposed;
80
+ // Saved the moment it exists, so a drive that breaks still leaves its plan behind.
81
+ const planFile = await savePlan(deps, options.sessionId, { planId, script });
82
+ const maxSteps = options.maxSteps ?? maxStepsFromEnv(env);
83
+ // The script replays the saved flows itself; a model handed them too replayed them again per goal.
84
+ const open = withoutReplays(plan);
85
+ // The journey itself leads (its rules are already written into it by the platform): a driver
86
+ // with nothing named to prove stopped after its first half.
87
+ const planFor = (goal) => {
88
+ if (goal === undefined)
89
+ return open;
90
+ const journey = { kind: PlanStepKind.DRIVE, target: 'this journey, to its end', why: goal };
91
+ return { ...open, steps: [journey, ...open.steps] };
92
+ };
93
+ const driverFor = (persona, goal) => options.driver === undefined
94
+ ? buildDriver(env, maxSteps, planFor(goal), options.driverName, persona, persona === undefined ? goal : undefined)
95
+ : { driver: options.driver, name: CUSTOM_DRIVER_NAME };
96
+ const driverName = driverFor().name;
97
+ const harness = randomUUID();
98
+ const narrate = narrator(deps, options);
99
+ // Lanes run side by side only in leased contexts; without a pool they take turns on this tab.
100
+ const pool = deps.pool;
101
+ const appUrl = leasableAppUrl(deps, options.sessionId);
102
+ const leasing = pool !== undefined && appUrl !== undefined && 1 < script.lanes.length;
103
+ const projectId = safeProjectId(deps, options.sessionId);
104
+ let off = false;
105
+ const poll = setInterval(() => {
106
+ void refusedByPlatform(env, options).then((refusal) => {
107
+ if (MSG_HARNESS_DISABLED === refusal)
108
+ off = true;
109
+ });
110
+ }, STOP_POLL_MS);
111
+ poll.unref();
112
+ const lanes = laneIds(harness, leasing);
113
+ // Keyed by the tool list, which the script runner's recording wrapper passes through unchanged.
114
+ const harnessOf = new WeakMap();
115
+ const ports = {
116
+ parallel: leasing ? Math.max(1, Math.min(script.lanes.length, pool.capacity(), MAX_LANES)) : 1,
117
+ stopped: () => off,
118
+ lease: async () => {
119
+ if (!leasing)
120
+ return { ...pinned(options), release: () => Promise.resolve() };
121
+ const acquire = () => acquireLeasedSession(pool, deps.sessions, appUrl, projectId);
122
+ // A cold dev server can miss the first load while other lanes compile it; the second finds it warm.
123
+ return acquire().catch(acquire);
124
+ },
125
+ toolset: (sessionId, persona) => {
126
+ const id = lanes.idFor(sessionId);
127
+ const toolset = reticleToolset(deps, {
128
+ ...(sessionId === undefined ? {} : { sessionId }),
129
+ drivenBy: {
130
+ harness: id,
131
+ driver: driverName,
132
+ ...(persona === undefined ? {} : { persona }),
133
+ },
134
+ });
135
+ harnessOf.set(toolset.tools, id);
136
+ return toolset;
137
+ },
138
+ drive: async (toolset, goal, steps) => {
139
+ const persona = focus ??
140
+ (script.personas.find((p) => goal.startsWith(`${p.name}:`)) === undefined
141
+ ? undefined
142
+ : goal);
143
+ const ahead = new Set(await reads.flows.list());
144
+ const result = await runHarness(driverFor(persona, goal).driver, toolset, {
145
+ maxSteps: Math.min(steps, maxSteps),
146
+ focus: [planAsText(planFor(goal)), `Focus: ${goal}`].join('\n\n'),
147
+ });
148
+ // Before the lane's session ends and is graded, so its run cannot read "Proved" over a goal
149
+ // the drive did not reach.
150
+ noteHarnessGoal(harnessOf.get(toolset.tools) ?? harness, result.goalMet);
151
+ await bankOpenRecording(toolset, result, persona);
152
+ if (persona !== undefined) {
153
+ const saved = reconcileFlows(ahead, await reads.flows.list(), result.toolCalls);
154
+ await recordPersona(reads, [...saved.savedFlows, ...saved.rewroteFlows], persona);
155
+ }
156
+ return result;
157
+ },
158
+ };
159
+ narrate(`Harness plan from the platform · ${String(script.journeys.length)} journeys in ${String(script.lanes.length)} lane(s)` +
160
+ (1 < ports.parallel ? `, ${String(ports.parallel)} at once` : ''));
161
+ let run;
162
+ try {
163
+ run = await runScript(script, ports, (view) => {
164
+ try {
165
+ deps.sessions.resolve(options.sessionId).pushView(ReticleCommand.PLAN, view);
166
+ }
167
+ catch {
168
+ /* nobody is watching: the plan still runs */
169
+ }
170
+ });
171
+ }
172
+ finally {
173
+ clearInterval(poll);
174
+ }
175
+ // The whole app, after the journeys: a crawl clicks every reachable control (nothing destructive)
176
+ // and finds what no journey is aimed at — a nav link that renders nothing, a dead control, an
177
+ // error in the console. On the merchant dashboard eight blank pages went unreported without it.
178
+ // After the journeys, so the lanes start sooner. (Run first, it once left every lane unable to
179
+ // start; that did not reproduce on a clean install, and was the checkout's, not the order's.)
180
+ const crawled = focus === undefined && !run.stopped
181
+ ? await crawlApp(deps, options, harness, driverName)
182
+ : undefined;
183
+ // How each journey went: kept beside the plan here, and told to the platform, whose next plan
184
+ // drives what failed first.
185
+ const results = journeyResults(run.view);
186
+ await savePlan(deps, options.sessionId, { planId, script, results, ...appControlsOf(run.toolCalls, crawled) }, planFile);
187
+ if (planId !== undefined)
188
+ await reportPlanResults(platform, planId, results);
189
+ for (const line of run.lines)
190
+ narrate(line);
191
+ narrate(run.stopped
192
+ ? 'Autonomous driving switched off — the Harness stopped. What it drove is kept.'
193
+ : `Harness finished the plan — ${String(run.lines.filter((l) => l.startsWith('✓')).length)} of ${String(run.lines.length)} journeys passed. ${verdictLine(checkTally(run.toolCalls))}`);
194
+ const sum = (pick) => run.drives.reduce((n, r) => n + pick(r), 0);
195
+ // A model drive that broke breaks the run: "finished" over a platform that answered 500 hid it.
196
+ const broke = run.drives.find((d) => StopReason.BROKEN === d.stopReason);
197
+ const drive = {
198
+ stopReason: run.stopped
199
+ ? StopReason.STOPPED
200
+ : broke !== undefined
201
+ ? StopReason.BROKEN
202
+ : StopReason.FINISHED,
203
+ ...(broke?.error === undefined ? {} : { error: broke.error }),
204
+ summary: run.drives
205
+ .map((d) => d.summary)
206
+ .filter((t) => 0 < t.length)
207
+ .join('\n'),
208
+ steps: run.toolCalls.length,
209
+ toolCalls: crawled === undefined ? run.toolCalls : [crawled, ...run.toolCalls],
210
+ usage: {
211
+ input: sum((r) => r.usage.input),
212
+ output: sum((r) => r.usage.output),
213
+ cacheRead: sum((r) => r.usage.cacheRead),
214
+ cacheWrite: sum((r) => r.usage.cacheWrite),
215
+ },
216
+ proved: run.view.lanes.some((lane) => lane.journeys.some((card) => ScriptStatus.PASSED === card.status)),
217
+ // Unmet if any journey's goal was judged unmet; met only when every judged one was.
218
+ ...goalOfRun(run.drives),
219
+ };
220
+ const reconciled = reconcileFlows(before, await reads.flows.list(), run.toolCalls);
221
+ const runIds = lanes.all().flatMap((id) => {
222
+ const runId = harnessRunId(id);
223
+ return runId === undefined ? [] : [runId];
224
+ });
225
+ const unverifiedFlows = await flowsThatCheckNothing(reads, [
226
+ ...reconciled.savedFlows,
227
+ ...reconciled.rewroteFlows,
228
+ ]);
229
+ return {
230
+ drive,
231
+ plan,
232
+ driverName,
233
+ ...reconciled,
234
+ unverifiedFlows,
235
+ // The texts a named journey quoted must be on the page at the end, checked as the persona
236
+ // drive always checked them.
237
+ goals: await checkGoals((name, args) => ports.toolset(pinned(options).sessionId, focus).invoke(name, args), options.goals ?? goalsIn(focus)),
238
+ ...(0 === runIds.length ? {} : { runIds }),
239
+ planLines: [
240
+ `Plan · ${String(script.journeys.length)} journeys in ${String(script.lanes.length)} lane(s)`,
241
+ ...run.lines,
242
+ ],
243
+ };
244
+ }
245
+ /** What the project still owes, in its own words: the user's recent request, then open intents. */
246
+ async function memoryGoals(deps, sessionId) {
247
+ const goals = [];
248
+ try {
249
+ const parsed = PromptContextSchema.safeParse(JSON.parse(await deps.fs.readFile(reticleDirPaths(sessionRoot(deps, sessionId)).request)));
250
+ const at = parsed.success ? parsed.data.at : undefined;
251
+ const request = parsed.success ? parsed.data.request : undefined;
252
+ if (request !== undefined && at !== undefined && deps.now() - at < REQUEST_FRESH_MS) {
253
+ goals.push(request);
254
+ }
255
+ }
256
+ catch {
257
+ /* no request declared: the common case */
258
+ }
259
+ try {
260
+ for (const intent of await openSessionIntents(deps, sessionId))
261
+ goals.push(intent.statement);
262
+ }
263
+ catch {
264
+ /* no intent ledger */
265
+ }
266
+ return [...new Set(goals.map((g) => g.trim().slice(0, MAX_GOAL_CHARS)))].filter((g) => 0 < g.length);
267
+ }
268
+ function safeProjectId(deps, sessionId) {
269
+ try {
270
+ return sessionTarget(deps, sessionId).projectId;
271
+ }
272
+ catch {
273
+ return undefined;
274
+ }
275
+ }
276
+ /**
277
+ * Keep a plan in `.reticle/plans/`, and how it went once it has run. The same file is rewritten
278
+ * with the results, so one plan is one file. Never fails the drive: a plan that cannot be written
279
+ * is still a plan that runs.
280
+ */
281
+ async function savePlan(deps, sessionId, plan, file) {
282
+ try {
283
+ const dir = reticleDirPaths(sessionRoot(deps, sessionId)).plans;
284
+ const path = file ?? join(dir, `${new Date(deps.now()).toISOString().replace(/[:.]/g, '-')}.json`);
285
+ await deps.fs.mkdir(dir);
286
+ await deps.fs.writeFile(path, `${JSON.stringify({ at: deps.now(), ...plan }, null, 2)}\n`);
287
+ return path;
288
+ }
289
+ catch {
290
+ return file;
291
+ }
292
+ }
293
+ /** The run's goal verdict: false if any drive missed its goal, true if all judged ones reached it. */
294
+ function goalOfRun(drives) {
295
+ const judged = drives.flatMap((d) => (d.goalMet === undefined ? [] : [d.goalMet]));
296
+ return 0 === judged.length ? {} : { goalMet: judged.every(Boolean) };
297
+ }
298
+ /** Controls the opening crawl clicks: enough for a dashboard's navigation and its main buttons. */
299
+ const CRAWL_STEPS = 30;
300
+ const CRAWL_ACTION = 'crawl';
301
+ /** The opening crawl, as one recorded call. Undefined when it could not run; the plan runs anyway. */
302
+ async function crawlApp(deps, options, harness, driver) {
303
+ // The crawl is an action of `reticle_verify` now; by its old name it answered "unknown tool",
304
+ // which read as a crawl that found nothing.
305
+ const args = { action: CRAWL_ACTION, maxSteps: CRAWL_STEPS };
306
+ try {
307
+ const result = await reticleToolset(deps, {
308
+ ...pinned(options),
309
+ drivenBy: { harness, driver },
310
+ }).invoke(ReticleTool.VERIFY, args);
311
+ if (undefined !== asRecord(result)['error'])
312
+ return undefined;
313
+ return { id: 'crawl', name: ReticleTool.CRAWL, args, result, isError: false };
314
+ }
315
+ catch {
316
+ return undefined;
317
+ }
318
+ }
319
+ /**
320
+ * The controls the last crawl saw, from the newest saved plan that kept them. The planner reads the
321
+ * start page only; without these it never learned the refund buttons on another page existed, and
322
+ * no journey went near the app's costliest defects.
323
+ */
324
+ async function controlsSeenBefore(deps, sessionId) {
325
+ try {
326
+ const dir = reticleDirPaths(sessionRoot(deps, sessionId)).plans;
327
+ const files = (await deps.fs.readdir(dir))
328
+ .filter((f) => f.endsWith('.json'))
329
+ .sort()
330
+ .reverse();
331
+ for (const file of files) {
332
+ const saved = asRecord(JSON.parse(await deps.fs.readFile(join(dir, file))));
333
+ const controls = saved['appControls'];
334
+ if (Array.isArray(controls))
335
+ return controls.filter((c) => 'string' === typeof c);
336
+ }
337
+ }
338
+ catch {
339
+ /* no plans yet: the planner works from the screen alone */
340
+ }
341
+ return [];
342
+ }
@@ -7,6 +7,7 @@
7
7
  * is missing a tool and THAT is the bug. Every capability a harness run proves is one a user's own
8
8
  * agent can reach.
9
9
  */
10
+ import { type DrivenBy } from '@reticlehq/core';
10
11
  import { type ToolDeps } from './tools.js';
11
12
  import type { HarnessToolset } from '../../features/harness/harness.js';
12
13
  export interface ReticleToolsetOptions {
@@ -14,6 +15,10 @@ export interface ReticleToolsetOptions {
14
15
  sessionId?: string;
15
16
  /** Restrict to these names. Defaults to the advertised surface plus the recording pair. */
16
17
  only?: readonly string[];
18
+ /** Stamped on every action this drive journals, so its verdicts fold into a run of their own. */
19
+ drivenBy?: DrivenBy;
20
+ /** The pinned session wins over any a call names: a remote caller never reaches another tab. */
21
+ locked?: boolean;
17
22
  }
18
23
  /**
19
24
  * Bind the live tool surface to a set of dependencies.
@@ -32,4 +37,12 @@ export declare function reticleToolset(deps: ToolDeps, options?: ReticleToolsetO
32
37
  * ids, and silently overwriting one would make a deliberate cross-tab call lie about which tab it
33
38
  * read.
34
39
  */
35
- export declare function pinSession(args: Record<string, unknown>, sessionId: string | undefined): Record<string, unknown>;
40
+ export declare function pinSession(args: Record<string, unknown>, sessionId: string | undefined, locked?: boolean): Record<string, unknown>;
41
+ /**
42
+ * A desktop app's backend is its IPC: a claim that the app writes to its backend means that call.
43
+ *
44
+ * Reported from a chat-requested drive of a Tauri app: the platform claimed a POST for Add, the app
45
+ * made the same write as `ipc://add_todo` (200), and the step was graded `no`. Only a MUTATING method
46
+ * is read this way; a read, an unbound request and every other kind keep their meaning.
47
+ */
48
+ export declare function desktopBackendClaims(args: Record<string, unknown>): Record<string, unknown>;
@@ -9,11 +9,12 @@
9
9
  */
10
10
  import { z } from 'zod';
11
11
  import { zodToJsonSchema } from 'zod-to-json-schema';
12
- import { ReticleTool } from '@reticlehq/core';
12
+ import { AppRuntime, ConsequenceKind, NetInitiator, ReticleTool, } from '@reticlehq/core';
13
13
  import { TOOLS } from './tools.js';
14
14
  import { runTool } from './invoke-tool.js';
15
15
  import { TOOL_SURFACE, filterTools } from './tool-surface.js';
16
16
  import { withTimeout } from '../../features/harness/with-timeout.js';
17
+ import { runDrivenBy } from '../../hooks/driven-by.js';
17
18
  /**
18
19
  * The two tools the harness is given beyond the advertised surface.
19
20
  *
@@ -43,6 +44,34 @@ const HARNESS_EXTRA_TOOLS = [
43
44
  * meant to protect.
44
45
  */
45
46
  const HARNESS_EXCLUDED = new Set([ReticleTool.FEEDBACK]);
47
+ /**
48
+ * All a locked (remote) drive may run: reading and driving the one tab it was given, and keeping or
49
+ * replaying what it drove there. Nothing that lists other tabs, opens a browser, spends the local
50
+ * model, deletes saved work or sends a picture the person did not ask to send.
51
+ */
52
+ const REMOTE_TOOLS = new Set([
53
+ ReticleTool.LOOK,
54
+ ReticleTool.SNAPSHOT,
55
+ ReticleTool.QUERY,
56
+ ReticleTool.INSPECT,
57
+ ReticleTool.NAVIGATE,
58
+ ReticleTool.REFRESH,
59
+ ReticleTool.ACT,
60
+ ReticleTool.ACT_AND_WAIT,
61
+ ReticleTool.ACT_SEQUENCE,
62
+ ReticleTool.OBSERVE,
63
+ ReticleTool.WAIT_FOR,
64
+ ReticleTool.WAIT_READY,
65
+ ReticleTool.SCROLL_TO,
66
+ ReticleTool.ASSERT,
67
+ ReticleTool.NETWORK,
68
+ ReticleTool.CONSOLE,
69
+ ReticleTool.STATE,
70
+ ReticleTool.RECORD,
71
+ ReticleTool.FLOW_SAVE,
72
+ ReticleTool.FLOW_REPLAY,
73
+ ReticleTool.RUN,
74
+ ]);
46
75
  /**
47
76
  * Parameters the model is never asked for, because the harness supplies them.
48
77
  *
@@ -93,7 +122,7 @@ function toHarnessTool(tool) {
93
122
  * a harness run a measurement of the product rather than of a test harness.
94
123
  */
95
124
  export function reticleToolset(deps, options = {}) {
96
- const usable = TOOLS.filter((tool) => !HARNESS_EXCLUDED.has(tool.name));
125
+ const usable = TOOLS.filter((tool) => !HARNESS_EXCLUDED.has(tool.name) && (true !== options.locked || REMOTE_TOOLS.has(tool.name)));
97
126
  /**
98
127
  * Everything callable, whether or not it is advertised. `reticle_run` dispatches against this, so
99
128
  * trimming the ADVERTISED list never trims what a drive can reach.
@@ -115,8 +144,14 @@ export function reticleToolset(deps, options = {}) {
115
144
  // list, and this is the same recovery the MCP surface gives an agent that does the same.
116
145
  return { error: `unknown tool ${target ?? name}`, available: [...byName.keys()] };
117
146
  }
118
- const scoped = pinSession(targetArgs, options.sessionId);
119
- return withTimeout(runTool(tool, deps, scoped), TOOL_TIMEOUT_MS, `${tool.name} never returned`);
147
+ const pinned = pinSession(targetArgs, options.sessionId, true === options.locked);
148
+ const scoped = isDesktopSession(deps, pinned['sessionId'])
149
+ ? desktopBackendClaims(pinned)
150
+ : pinned;
151
+ const drivenBy = options.drivenBy;
152
+ return withTimeout(drivenBy === undefined
153
+ ? runTool(tool, deps, scoped)
154
+ : runDrivenBy(drivenBy, () => runTool(tool, deps, scoped)), TOOL_TIMEOUT_MS, `${tool.name} never returned`);
120
155
  },
121
156
  };
122
157
  }
@@ -129,11 +164,47 @@ export function reticleToolset(deps, options = {}) {
129
164
  * ids, and silently overwriting one would make a deliberate cross-tab call lie about which tab it
130
165
  * read.
131
166
  */
132
- export function pinSession(args, sessionId) {
133
- if (sessionId === undefined || 'sessionId' in args)
167
+ export function pinSession(args, sessionId, locked = false) {
168
+ if (sessionId === undefined || (!locked && 'sessionId' in args))
134
169
  return args;
135
170
  return { ...args, sessionId };
136
171
  }
172
+ /** The HTTP methods a "saves" or "updates" claim names: a write to the app's backend. */
173
+ const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
174
+ function isDesktopSession(deps, sessionId) {
175
+ try {
176
+ const runtime = deps.sessions.resolve('string' === typeof sessionId ? sessionId : undefined).runtime;
177
+ return AppRuntime.ELECTRON === runtime || AppRuntime.TAURI === runtime;
178
+ }
179
+ catch {
180
+ return false;
181
+ }
182
+ }
183
+ /**
184
+ * A desktop app's backend is its IPC: a claim that the app writes to its backend means that call.
185
+ *
186
+ * Reported from a chat-requested drive of a Tauri app: the platform claimed a POST for Add, the app
187
+ * made the same write as `ipc://add_todo` (200), and the step was graded `no`. Only a MUTATING method
188
+ * is read this way; a read, an unbound request and every other kind keep their meaning.
189
+ */
190
+ export function desktopBackendClaims(args) {
191
+ const rewrite = (value) => {
192
+ if (Array.isArray(value))
193
+ return value.map(rewrite);
194
+ if ('object' !== typeof value || null === value)
195
+ return value;
196
+ const record = value;
197
+ const method = record['method'];
198
+ const ipc = ConsequenceKind.NET === record['kind'] &&
199
+ 'string' === typeof method &&
200
+ MUTATING_METHODS.has(method.toUpperCase());
201
+ return Object.fromEntries(Object.entries(record).map(([key, inner]) => [
202
+ key,
203
+ ipc && 'method' === key ? NetInitiator.IPC : rewrite(inner),
204
+ ]));
205
+ };
206
+ return rewrite(args);
207
+ }
137
208
  function asName(value) {
138
209
  return 'string' === typeof value && value.length > 0 ? value : undefined;
139
210
  }
@@ -1,4 +1,9 @@
1
1
  import { healthEnvelope } from '../../portal/session/session-health.js';
2
+ import { logToolCall, toolLogPath } from '../../hooks/tool-log.js';
3
+ import { nextStep } from './next-step.js';
4
+ import { currentDrivenBy } from '../../hooks/driven-by.js';
5
+ import { sessionRoot } from '../../memory/project/session-root.js';
6
+ import { takePlatformMoment } from './platform-moment.js';
2
7
  import { verifyNextBaton, SUPPRESS_VERIFY_NEXT_ENV } from './verify-next-baton.js';
3
8
  import { TelemetryActor, TelemetryEventKind, TRANSPORT_LIMITS, fingerprintFinding, } from '@reticlehq/core';
4
9
  import { getSessionMetrics } from '../../telemetry/session-metrics.js';
@@ -340,6 +345,22 @@ function pinnedArgs(args, explicitSessionId, session) {
340
345
  return { ...args, [SESSION_ID_ARG]: session.id };
341
346
  }
342
347
  export async function runTool(tool, deps, args) {
348
+ const logPath = toolLogPath();
349
+ if (logPath === undefined)
350
+ return dispatchTool(tool, deps, args);
351
+ const at = deps.now();
352
+ try {
353
+ const result = await dispatchTool(tool, deps, args);
354
+ logToolCall(logPath, { tool: tool.name, args, at, ms: deps.now() - at, result });
355
+ return result;
356
+ }
357
+ catch (error) {
358
+ const message = error instanceof Error ? error.message : String(error);
359
+ logToolCall(logPath, { tool: tool.name, args, at, ms: deps.now() - at, error: message });
360
+ throw error;
361
+ }
362
+ }
363
+ async function dispatchTool(tool, deps, args) {
343
364
  // Both dispatch paths (MCP + programmatic) pass through here — the one place "which tool is mostly
344
365
  // used" can be counted. This used to EMIT an event per call; it now increments an in-process counter
345
366
  // that leaves once, with the session summary. A verification loop is 50–200 calls, and PostHog bills
@@ -605,13 +626,33 @@ export async function runTool(tool, deps, args) {
605
626
  // RETURN an error. So the denominator was counting a small, unrepresentative slice of the
606
627
  // invitations and reading near-empty, which looks identical to a nudge that never fires.
607
628
  const friction = frictionInviteFor(tool.name, raw);
629
+ // Once per project, on a proved verdict, when the runs live only on this machine.
630
+ const platform = isPlainObject(raw)
631
+ ? await takePlatformMoment(deps, raw, () => sessionRoot(deps, rawSessionId))
632
+ : undefined;
633
+ // What the agent should do next, on every client that reads results. Not for the Harness, whose
634
+ // runs the daemon syncs itself and which cannot relay the user's words, and not on `reticle_run`'s
635
+ // outer result: the inner call already carried one.
636
+ const next = isPlainObject(raw) && ReticleTool.RUN !== tool.name && currentDrivenBy() === undefined
637
+ ? nextStep({
638
+ tool: tool.name,
639
+ verdict: 'verified' in raw,
640
+ root: safeRoot(() => sessionRoot(deps, rawSessionId)),
641
+ // A partial deps (tests, the demo) may carry no clock; the line is advice, never a reason to fail.
642
+ now: 'function' === typeof deps.now ? deps.now() : Date.now(),
643
+ })
644
+ : undefined;
608
645
  const result = prompt === undefined &&
609
646
  update === undefined &&
610
647
  skew === undefined &&
611
648
  undelivered === undefined &&
612
- friction === undefined
649
+ friction === undefined &&
650
+ platform === undefined &&
651
+ next === undefined
613
652
  ? raw
614
653
  : {
654
+ // First, so a model that truncates a long result still reads it.
655
+ ...(next !== undefined ? { [EnvelopeKey.NEXT]: next } : {}),
615
656
  ...raw,
616
657
  ...(friction !== undefined
617
658
  ? {
@@ -623,6 +664,7 @@ export async function runTool(tool, deps, args) {
623
664
  ...(prompt !== undefined ? { [EnvelopeKey.FEEDBACK_PROMPT]: prompt } : {}),
624
665
  ...(update !== undefined ? { [EnvelopeKey.UPDATE_AVAILABLE]: update } : {}),
625
666
  ...(skew !== undefined ? { [EnvelopeKey.VERSION_SKEW]: skew } : {}),
667
+ ...(platform !== undefined ? { [EnvelopeKey.PLATFORM]: platform } : {}),
626
668
  ...(undelivered !== undefined
627
669
  ? {
628
670
  [EnvelopeKey.FEEDBACK_UNDELIVERED]: `your earlier report did NOT send: ${undelivered}. Tell the human what you found so it is not lost.`,
@@ -683,3 +725,12 @@ export async function runTool(tool, deps, args) {
683
725
  envelope[EnvelopeKey.VERIFY_NEXT] = verifyNextBaton(unverified, resolved.lastAct?.effect() ?? {});
684
726
  return Object.keys(envelope).length > 0 ? { ...result, ...envelope } : result;
685
727
  }
728
+ /** A root, or undefined when none can be resolved: a missing root is never a reason to throw. */
729
+ function safeRoot(get) {
730
+ try {
731
+ return get();
732
+ }
733
+ catch {
734
+ return undefined;
735
+ }
736
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Seeded storage for a lease, and the errors a lease's navigation throws: what a caller may see of
3
+ * them without seeing the secrets it seeded. Split from lease-tools.ts, which crossed the line cap.
4
+ */
5
+ import { type SeedStorage } from '@reticlehq/core';
6
+ /**
7
+ * Scrub known secret shapes and raw seedStorage values (cookies, localStorage, sessionStorage)
8
+ * from an error string before it reaches daemon logs, metrics, or MCP error responses.
9
+ */
10
+ export declare function scrubSeedFromError(text: string, seed?: unknown): string;
11
+ /** True for a Playwright `storageState()` export (`{ origins: [...] }`), not our seed shape. */
12
+ export declare function looksLikeStorageStateExport(seed: unknown): boolean;
13
+ /**
14
+ * Turn a raw navigation failure (Playwright's `page.goto: net::ERR_… at <url>\nCall log:…`, often
15
+ * with ANSI codes) into a short, clean reason — so the agent/user sees "is the app running?" instead
16
+ * of an internals-leaking wall of text.
17
+ */
18
+ export declare function cleanNavError(err: unknown, seed?: SeedStorage): string;
19
+ /**
20
+ * Determine if a required seedStorage application precondition failed.
21
+ *
22
+ * Checks strong signals:
23
+ * 1. HTTP 401/403 status on the initial navigation.
24
+ * 2. Cross-origin redirect when localStorage or sessionStorage was requested (skipped by origin-scoping).
25
+ * 3. Clear redirect to an authentication/login endpoint when the requested URL was not a login page.
26
+ *
27
+ * Normal app redirects (e.g. / → /dashboard) are valid and return undefined.
28
+ */
29
+ export declare function evaluateSeedPrecondition(requestedUrl: string, actualUrl: string | undefined, seed: SeedStorage, navStatus?: number): string | undefined;