@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
@@ -7,22 +7,24 @@
7
7
  * nothing grades) but the flows that now exist on disk, because every future run replays those
8
8
  * deterministically with no model in the loop at all.
9
9
  */
10
- import { ReticleEnv, ReticleTool, cloudUrlFrom, asRecord } from '@reticlehq/core';
10
+ import { serverDriver, serverOptionsFromEnv } from '../../features/harness/platform/server-driver.js';
11
+ import { exploreScript } from './harness-script.js';
12
+ import { randomUUID } from 'node:crypto';
13
+ import { harnessRunId } from '../../judgement/runs/drive-run.js';
14
+ import { ReticleEnv, ReticleTool, asProjectId, cloudUrlFrom, asRecord, } from '@reticlehq/core';
15
+ import { flowsForSession } from '../../language/flows/flow-store-for-session.js';
11
16
  import { projectForRoot } from '../../memory/project/project-for-root.js';
12
- import { harnessDriver, harnessOptionsFromEnv, } from '../../features/harness/driver.js';
13
- import { fillValues } from '../../features/harness/fill-values.js';
14
- import { openFillValues } from '../../memory/project/dir/fill-value-store.js';
15
- import { ANTHROPIC_DRIVER_NAME, CUSTOM_DRIVER_NAME, DRIVER_NAMES, JEV_DRIVER_NAME, OPENAI_DRIVER_NAME, } from '../../features/harness/drivers.js';
16
- import { jevDriver, jevOptionsFromEnv } from '../../features/harness/jev-driver.js';
17
+ import { checkTally, verdictLine } from '../../features/harness/drive-report.js';
18
+ import { CUSTOM_DRIVER_NAME, SERVER_DRIVER } from '../../features/harness/drivers.js';
17
19
  import { buildDomainModel } from '../../judgement/domain/domain-model.js';
18
20
  import { readContract } from '../../memory/project/dir/reticle-dir.js';
19
- import { sessionRoot } from '../../memory/project/session-root.js';
20
- import { buildHarnessPlan, planAsText } from './harness-plan.js';
21
- import { openAiDriver, openAiOptionsFromEnv, } from '../../features/harness/openai-driver.js';
21
+ import { sessionRoot, sessionTarget } from '../../memory/project/session-root.js';
22
+ import { buildHarnessPlan, planAsText, withoutReplays } from './harness-plan.js';
23
+ import { allSessionIntents } from '../../memory/intent/open-intents.js';
22
24
  import { fetchPlatformConfig } from '../../features/harness/platform-config.js';
23
- import { DEFAULT_MAX_STEPS, runHarness, } from '../../features/harness/harness.js';
25
+ import { DEFAULT_MAX_STEPS, runHarness, StopReason, } from '../../features/harness/harness.js';
24
26
  import { reticleToolset } from './harness-toolset.js';
25
- import { secretEnvKey } from '../../language/flows/flows.js';
27
+ import { checkGoals, goalsIn } from '../../features/harness/goals.js';
26
28
  /**
27
29
  * Why the harness is unavailable, phrased as the way to make it available.
28
30
  *
@@ -30,18 +32,13 @@ import { secretEnvKey } from '../../language/flows/flows.js';
30
32
  * the one thing to do about it. Named here because both the CLI and the honest no-flows refusal
31
33
  * print it, and two copies of this sentence would drift.
32
34
  */
33
- export const MSG_NO_HARNESS_KEY = `No model configured to drive the app: set ${ReticleEnv.HARNESS_KEY}, or run \`reticle link\` and ` +
34
- `set ${ReticleEnv.CLOUD_KEY}, to let Reticle explore it for you. Without either, flows are ` +
35
- `recorded by your own coding agent through the MCP tools.`;
36
- /** Asked for Jev specifically and it is not configured. Distinct from having no model at all. */
37
- export const MSG_NO_JEV_KEY = `The \`${JEV_DRIVER_NAME}\` driver was asked for but is not configured: set ` +
38
- `${ReticleEnv.HARNESS_JEV_KEY}, or run \`reticle link\` and set ${ReticleEnv.CLOUD_KEY}. ` +
39
- `Refusing rather than quietly driving with another model, which would misattribute the result.`;
40
- /** Asked for OpenAI specifically and it is not configured. Never substituted, same as Jev. */
41
- export const MSG_NO_OPENAI_KEY = `The \`${OPENAI_DRIVER_NAME}\` driver was asked for but is not configured: set ` +
42
- `${ReticleEnv.HARNESS_OPENAI_KEY}, or run \`reticle link\` and set ${ReticleEnv.CLOUD_KEY}. ` +
43
- `Refusing rather than quietly driving with another model, which would misattribute the result.`;
44
- const msgUnknownDriver = (asked) => `Unknown harness driver \`${asked}\`. Known drivers: ${DRIVER_NAMES.join(', ')}.`;
35
+ export const MSG_NO_HARNESS_KEY = `The Reticle Harness runs on the Reticle platform, and every plan includes it (Free comes with ` +
36
+ `monthly Harness credits): run \`reticle connect\` to link this project and sign in. ` +
37
+ `Without it, drive the journey yourself with reticle_act_and_wait and an \`until\` on its last step: ` +
38
+ `what you drive is saved as a flow just the same.`;
39
+ /** A driver named that this build no longer has: every Harness decision is the platform's now. */
40
+ const msgUnknownDriver = (asked) => `The Harness driver \`${asked}\` is not available: the Harness runs on the Reticle platform, ` +
41
+ `which drives with the model set for the project in the dashboard.`;
45
42
  /**
46
43
  * The environment, plus the credential this machine already has.
47
44
  *
@@ -78,11 +75,12 @@ export async function withLinkedCredential(deps, env) {
78
75
  return env;
79
76
  }
80
77
  }
81
- /** Is there a model the harness can drive with? A read, because "not configured" is not a failure. */
78
+ /**
79
+ * Can this machine reach the Harness? Only through the platform: a linked project or a platform
80
+ * key. Whether the workspace is entitled is the platform's answer, asked when the drive starts.
81
+ */
82
82
  export function harnessAvailable(env) {
83
- return (harnessOptionsFromEnv(env) !== undefined ||
84
- jevOptionsFromEnv(env) !== undefined ||
85
- openAiOptionsFromEnv(env) !== undefined);
83
+ return serverOptionsFromEnv(env) !== undefined;
86
84
  }
87
85
  /**
88
86
  * Read the step ceiling out of the environment.
@@ -110,48 +108,160 @@ export async function exploreApp(deps, env, options = {}) {
110
108
  const refusal = await refusedByPlatform(env, options);
111
109
  if (refusal !== undefined)
112
110
  throw new Error(refusal);
111
+ // The flows a drive reads and diffs are the PROJECT's, which is where its saves land.
112
+ const reads = withProjectFlows(deps, options.sessionId);
113
+ // No journey named: plan the drive first. The platform's Harness proposes the people worth being;
114
+ // the saved flows, the user's request and the unproved intents fill in the rest. The plan is drawn
115
+ // on the HUD and updated as each part runs.
116
+ const before = new Set(await reads.flows.list());
117
+ // Without a named journey the drive is always planned; with one, it is planned by the platform
118
+ // when there is one, and driven here as before when there is not.
119
+ if (options.focus === undefined || serverOptionsFromEnv(env) !== undefined) {
120
+ const scripted = await exploreScript(deps, env, options, before);
121
+ if (scripted !== undefined)
122
+ return scripted;
123
+ }
113
124
  const maxSteps = options.maxSteps ?? maxStepsFromEnv(env);
114
- const before = new Set(await deps.flows.list());
115
125
  // `.reticle` FIRST, before the app and before anything reads a line of source. It already holds
116
126
  // every saved flow, the consequence that must hold for each, and the declared intent nobody has
117
127
  // tested — which is the whole of what a drive should be deciding against.
118
- const plan = await readPlan(deps, options.sessionId);
119
- /*
120
- * The fixtures this project has already paid for.
121
- *
122
- * A drive is a sequence of DECISIONS and, occasionally, one COMPOSITION: what to type into a box
123
- * whose label nothing can guess from. The cheap driver cannot write a string at all, so those
124
- * boxes used to receive the words "reticle harness". This is the seam where a generating model
125
- * answers instead -- for about 6% of steps, once per label, and never again on any later drive,
126
- * because the answer is written into `.reticle` beside the flows that use it.
127
- */
128
- const fills = await openFillValues(deps.fs, safeRoot(deps, options.sessionId));
129
- const requested = options.driverName ?? env[ReticleEnv.HARNESS_DRIVER] ?? (await preferredDriver(env, options));
128
+ const plan = await readPlan(reads, options.sessionId);
129
+ const driving = withoutReplays(plan);
130
130
  const built = options.driver === undefined
131
- ? buildDriver(env, maxSteps, plan, requested, fills)
131
+ ? buildDriver(env, maxSteps, driving, options.driverName, options.focus)
132
132
  : { driver: options.driver, name: CUSTOM_DRIVER_NAME };
133
133
  const driver = built.driver;
134
- const toolset = reticleToolset(deps, pinned(options));
134
+ // Every action this drive takes carries who took it, so the run it folds into is the Harness's own
135
+ // and not mixed into the run of whichever agent shares the tab.
136
+ const harnessId = randomUUID();
137
+ const toolset = reticleToolset(deps, {
138
+ ...pinned(options),
139
+ drivenBy: {
140
+ harness: harnessId,
141
+ driver: built.name,
142
+ ...(options.focus === undefined ? {} : { persona: options.focus }),
143
+ },
144
+ });
145
+ const narrate = narrator(deps, options);
146
+ narrate(`Harness is driving${options.focus === undefined ? '' : `: ${options.focus}`}`);
135
147
  const drive = await runHarness(driver, toolset, {
136
148
  maxSteps,
137
149
  // The plan rides in as standing instruction, so it is in front of the model on every turn
138
150
  // rather than remembered from a first one. `focus` is the caller's own words and goes last:
139
151
  // somebody who named a journey meant that journey, whatever the project's backlog says.
140
152
  focus: [
141
- planAsText(plan),
153
+ planAsText(driving),
142
154
  ...(options.focus === undefined ? [] : [`Focus: ${options.focus}`]),
143
155
  ].join('\n\n'),
144
156
  });
145
- // Written once, after the drive, whatever the drive did: a run that broke still learned what it
146
- // learned, and the next one should not pay for it again.
147
- await fills.flush();
157
+ narrate(StopReason.STOPPED === drive.stopReason
158
+ ? `Autonomous driving switched off — the Harness stopped after ${String(drive.steps)} steps. What it drove is kept.`
159
+ : `Harness finished — ${verdictLine(checkTally(drive.toolCalls))}`);
148
160
  // MANDATORY, and deliberately outside the loop. A drive that runs out of budget mid-journey, or
149
161
  // breaks, or whose model simply stops asking for tools, leaves a recording open and everything it
150
162
  // drove unsaved — work paid for and thrown away. Saving is not a decision any model gets to make
151
163
  // and not something a step budget gets to cut off, so it happens here, after the loop, always.
152
- await bankOpenRecording(toolset, drive);
153
- const after = await deps.flows.list();
154
- return { drive, plan, driverName: built.name, ...reconcileFlows(before, after, drive.toolCalls) };
164
+ await bankOpenRecording(toolset, drive, options.focus);
165
+ const goals = await checkGoals((name, args) => toolset.invoke(name, args), options.goals ?? goalsIn(options.focus));
166
+ const after = await reads.flows.list();
167
+ const reconciled = reconcileFlows(before, after, drive.toolCalls);
168
+ if (options.focus !== undefined) {
169
+ await recordPersona(reads, [...reconciled.savedFlows, ...reconciled.rewroteFlows], options.focus);
170
+ }
171
+ const unverifiedFlows = await flowsThatCheckNothing(reads, [
172
+ ...reconciled.savedFlows,
173
+ ...reconciled.rewroteFlows,
174
+ ]);
175
+ const runId = harnessRunId(harnessId);
176
+ return {
177
+ drive,
178
+ plan,
179
+ driverName: built.name,
180
+ ...reconciled,
181
+ unverifiedFlows,
182
+ goals,
183
+ ...(runId === undefined ? {} : { runIds: [runId] }),
184
+ };
185
+ }
186
+ /** A line in the HUD's Agent Log for the person watching; nobody watching is not an error. */
187
+ export function narrator(deps, options) {
188
+ return (text) => {
189
+ try {
190
+ deps.sessions.resolve(options.sessionId).pushNarration(text);
191
+ }
192
+ catch {
193
+ /* nobody is watching: the drive still runs */
194
+ }
195
+ };
196
+ }
197
+ /** What this project's memory says must hold, in its own words. Nothing recorded is none. */
198
+ export async function productRules(deps, sessionId) {
199
+ try {
200
+ return (await allSessionIntents(deps, sessionId)).map((intent) => intent.statement);
201
+ }
202
+ catch {
203
+ return [];
204
+ }
205
+ }
206
+ /** `deps` whose flow store is the session's own project, where the drive's saves land. */
207
+ export function withProjectFlows(deps, sessionId) {
208
+ try {
209
+ return { ...deps, flows: flowsForSession(deps, sessionTarget(deps, sessionId)).flows };
210
+ }
211
+ catch {
212
+ return deps;
213
+ }
214
+ }
215
+ /**
216
+ * Write the persona onto every flow the drive saved or rewrote without an intent.
217
+ *
218
+ * Whichever driver saved it, and in whatever words: the cheap driver's own teardown saved its flow
219
+ * with no intent at all, so a replay could never say which journey it was meant to prove. A flow
220
+ * that already carries an intent keeps it. Best effort: a flow that cannot be read or rewritten is
221
+ * still a saved flow.
222
+ */
223
+ export async function recordPersona(deps, names, persona) {
224
+ // Only the journey itself, "Name: journey": the plan appends the product's rules below it, and
225
+ // keeping them made each plan append them again to a goal that already carried them.
226
+ const intent = (persona.split('\n')[0] ?? '').trim();
227
+ if (0 === intent.length)
228
+ return;
229
+ for (const name of names) {
230
+ try {
231
+ const loaded = await deps.flows.load(name);
232
+ if (!loaded.ok || (loaded.value.intent ?? '').length > 0)
233
+ continue;
234
+ // Its own project: without it the store files a flat duplicate beside the original.
235
+ const project = loaded.value.projectId;
236
+ await deps.flows.saveFlow({ ...loaded.value, intent }, project === undefined ? undefined : asProjectId(project));
237
+ }
238
+ catch {
239
+ /* the flow stays as it was saved */
240
+ }
241
+ }
242
+ }
243
+ /**
244
+ * The saved flows with no step that asserts a consequence.
245
+ *
246
+ * Such a flow replays as "verified nothing": it clicks through the journey and would pass with the
247
+ * feature broken. Named so the drive's report does not count it as evidence. A flow that cannot be
248
+ * read is left out rather than guessed at.
249
+ */
250
+ export async function flowsThatCheckNothing(deps, names) {
251
+ const empty = [];
252
+ for (const name of names) {
253
+ try {
254
+ const loaded = await deps.flows.load(name);
255
+ if (!loaded.ok)
256
+ continue;
257
+ if (!loaded.value.steps.some((step) => step.expect !== undefined))
258
+ empty.push(name);
259
+ }
260
+ catch {
261
+ /* unreadable is not the same as empty */
262
+ }
263
+ }
264
+ return empty;
155
265
  }
156
266
  /**
157
267
  * What a drive actually left behind, split into flows that are NEW and flows it wrote over.
@@ -188,28 +298,6 @@ export function reconcileFlows(before, after, toolCalls) {
188
298
  rewroteFlows: [...new Set(claimed.filter((name) => before.has(name)))],
189
299
  };
190
300
  }
191
- /**
192
- * The driver this project chose on the platform, if it chose one, we can reach it, and we have it.
193
- *
194
- * Only asked when nobody has been explicit, so the cost is paid exactly once per drive and never on
195
- * a path where it could not change the answer. An unreachable platform returns undefined and the
196
- * environment decides, exactly as it did before this lookup existed.
197
- *
198
- * A preference naming a driver this build does not have is IGNORED rather than refused, and that
199
- * asymmetry with the per-call argument is deliberate. The platform offers providers this daemon may
200
- * be too old to know about — it already offers `openai`, which has no binding here — so a stored
201
- * preference is a statement about the account, not an instruction for this drive, and a daemon that
202
- * refused to run because a web UI knew one more word than it does would be broken by its own
203
- * upgrade cycle. Naming a driver in the CALL is an instruction, and an unknown one is still an
204
- * error there.
205
- *
206
- * It is not a silent substitution either way: the result reports the driver that actually drove.
207
- */
208
- async function preferredDriver(env, options) {
209
- if (true === options.skipPlatformConfig)
210
- return undefined;
211
- return knownDriver((await platformConfig(env, options))?.provider);
212
- }
213
301
  /**
214
302
  * What a person said about autonomous driving on the platform, honoured here.
215
303
  *
@@ -226,30 +314,14 @@ export const MSG_HARNESS_DISABLED = 'Autonomous driving is turned OFF for this p
226
314
  /**
227
315
  * The other reason a drive can be refused, and it is NOT the same reason.
228
316
  *
229
- * A drive through the platform spends Reticle's model budget. That is free for three months and
230
- * included on a paid plan, and outside both it is somebody else's money being spent on nothing. The
231
- * message says which of the two it is and what to do, because "the harness is off" told to a person
232
- * who never turned anything off is a support ticket rather than an answer.
317
+ * A drive through the platform spends Reticle's model budget, bounded by the workspace's monthly
318
+ * Harness credits. The platform answers this only when something other than the switch stops it.
233
319
  */
234
- export const MSG_HARNESS_UNCLAIMED = 'This workspace has no harness entitlement, so autonomous driving would run on Reticle’s model ' +
235
- 'budget with nothing paying for it. Claim the free 3 months in the Reticle dashboard, or export a ' +
236
- 'model API key of your own and drive with that.';
237
- /**
238
- * Whether a drive would be paid for by the person asking for it.
239
- *
240
- * Entitlement gates OUR spend, so it has no business stopping somebody who brought their own key:
241
- * their harness costs us nothing whether they ever claim anything or not. Reading the same options
242
- * the drivers read keeps the two answers from drifting apart.
243
- */
244
- function ownsAModelKey(env) {
245
- // The DIRECT variables only. Every driver also accepts the platform key as a fallback, so asking
246
- // `jevOptionsFromEnv` here would answer "they have a key" for exactly the person whose drive would
247
- // be billed to us — which is the one case this gate exists for.
248
- return [ReticleEnv.HARNESS_KEY, ReticleEnv.HARNESS_JEV_KEY, ReticleEnv.HARNESS_OPENAI_KEY].some((name) => {
249
- const value = env[name];
250
- return value !== undefined && 0 < value.length;
251
- });
252
- }
320
+ export const MSG_HARNESS_UNCLAIMED = 'The Reticle platform says this workspace cannot drive the Harness right now. Every workspace, ' +
321
+ 'Free included, gets Harness credits each month: see Settings → Plan in the Reticle dashboard.';
322
+ /** The platform could not be asked, and the drive would spend Reticle's budget without its yes. */
323
+ export const MSG_HARNESS_UNCONFIRMED = 'Could not confirm Harness access with the Reticle platform, so the drive did not start. Try ' +
324
+ 'again in a moment.';
253
325
  /** One read, however many questions are asked of the answer. */
254
326
  function platformConfig(env, options) {
255
327
  return options.configFetch === undefined
@@ -259,41 +331,31 @@ function platformConfig(env, options) {
259
331
  /**
260
332
  * The reason this drive must not start, or `undefined` to go ahead.
261
333
  *
262
- * It FAILS OPEN, and that is worth stating plainly because the argument for it changed under it.
263
- * `fetchPlatformConfig` answers `undefined` on a network error, a non-2xx, a body that does not
264
- * parse, or a two-second timeout, and all four land here as "carry on". When this endpoint only
265
- * answered "which model does this project prefer", failing open cost nothing: the drive used the
266
- * environment and nobody was worse off. It now also answers "is anybody paying for this", and the
267
- * same silence means a drive runs on Reticle's model budget unmetered.
268
- *
269
- * Kept open anyway, deliberately: a settings endpoint having a slow second must not break somebody
270
- * mid-verification, and the exposure is one drive's worth of a very cheap model. But this is
271
- * best-effort, not enforcement -- if the spend needs a hard floor, it belongs at the proxy, which
272
- * is the only side that cannot be talked out of it by a dropped packet.
334
+ * A drive on somebody's OWN model key never waits on the platform beyond the off switch: their key,
335
+ * their spend. A drive through the platform proxy bills Reticle, and that one needs a confirmed yes
336
+ * — enabled, entitled, and a model ready. It used to fail open: `fetchPlatformConfig` answers
337
+ * `undefined` on a network error, a non-2xx, a body that does not parse, or a two-second timeout,
338
+ * and all four read as "carry on", so a slow second on a settings endpoint let an unentitled
339
+ * workspace drive on Reticle's budget. Now the same silence refuses with a message that says to
340
+ * retry or bring a key. Fields an older platform omits still default to yes inside
341
+ * `fetchPlatformConfig`; only an answer that never arrived is a no.
273
342
  */
274
- async function refusedByPlatform(env, options) {
343
+ export async function refusedByPlatform(env, options) {
275
344
  if (true === options.skipPlatformConfig)
276
345
  return undefined;
346
+ // No platform, no Harness: it runs there, on the workspace's credits.
347
+ if (serverOptionsFromEnv(env) === undefined)
348
+ return MSG_NO_HARNESS_KEY;
277
349
  const config = await platformConfig(env, options);
350
+ // It needs a confirmed yes, not the absence of a no. The platform also checks on every turn.
278
351
  if (config === undefined)
279
- return undefined;
352
+ return MSG_HARNESS_UNCONFIRMED;
280
353
  if (!config.harnessEnabled)
281
354
  return MSG_HARNESS_DISABLED;
282
- if (!config.harnessEntitled && !ownsAModelKey(env))
355
+ if (!config.harnessEntitled)
283
356
  return MSG_HARNESS_UNCLAIMED;
284
357
  return undefined;
285
358
  }
286
- /**
287
- * A stored preference, kept only if this build can actually honour it.
288
- *
289
- * Exported because it is the whole of the asymmetry with the per-call argument, and the asymmetry
290
- * is the part somebody will later think is a bug.
291
- */
292
- export function knownDriver(provider) {
293
- if (provider === undefined)
294
- return undefined;
295
- return DRIVER_NAMES.some((name) => name === provider) ? provider : undefined;
296
- }
297
359
  /**
298
360
  * Close and save whatever recording the drive left open.
299
361
  *
@@ -306,7 +368,19 @@ export function knownDriver(provider) {
306
368
  * already closed the stop is refused and there is nothing to do, and a drive must not fail at the
307
369
  * finish line because the thing it was trying to rescue did not need rescuing.
308
370
  */
309
- async function bankOpenRecording(toolset, drive) {
371
+ /**
372
+ * What a banked recording says it was for: the persona the caller named, when there was one.
373
+ *
374
+ * It used to be "Autonomous drive of X, banked after the run ended" whatever the caller asked for,
375
+ * so a replay could never say which journey the flow was meant to prove.
376
+ */
377
+ export function bankedIntent(persona, open) {
378
+ const named = persona?.trim();
379
+ return named !== undefined && 0 < named.length
380
+ ? named
381
+ : `Autonomous drive of ${open}, banked after the run ended.`;
382
+ }
383
+ export async function bankOpenRecording(toolset, drive, persona) {
310
384
  const open = openRecordingName(drive.toolCalls);
311
385
  if (open === undefined)
312
386
  return;
@@ -314,7 +388,7 @@ async function bankOpenRecording(toolset, drive) {
314
388
  await toolset.invoke(ReticleTool.RECORD, { action: 'stop', recordingName: open });
315
389
  await toolset.invoke(ReticleTool.FLOW_SAVE, {
316
390
  flowName: open,
317
- intent: `Autonomous drive of ${open}, banked after the run ended.`,
391
+ intent: bankedIntent(persona, open),
318
392
  });
319
393
  }
320
394
  catch {
@@ -345,14 +419,23 @@ export function openRecordingName(toolCalls) {
345
419
  * ordinary state of a new project and not a reason to refuse to drive — so it answers an empty
346
420
  * plan rather than throwing.
347
421
  */
348
- async function readPlan(deps, sessionId) {
422
+ export async function readFlows(deps) {
423
+ const flows = [];
349
424
  try {
350
- const flows = [];
351
425
  for (const name of await deps.flows.list()) {
352
426
  const loaded = await deps.flows.load(name);
353
427
  if (loaded.ok)
354
428
  flows.push(loaded.value);
355
429
  }
430
+ }
431
+ catch {
432
+ // An unreadable store plans from what it could read, as `readPlan` does.
433
+ }
434
+ return flows;
435
+ }
436
+ export async function readPlan(deps, sessionId) {
437
+ try {
438
+ const flows = await readFlows(deps);
356
439
  const root = sessionRoot(deps, sessionId);
357
440
  const contract = await readContract(deps.fs, root);
358
441
  const project = await projectForRoot(deps, root).read();
@@ -366,93 +449,31 @@ async function readPlan(deps, sessionId) {
366
449
  };
367
450
  }
368
451
  }
369
- /**
370
- * The project directory, or nothing.
371
- *
372
- * `sessionRoot` needs a session and a project to answer, and a drive can legitimately be asked for
373
- * before either is resolvable. Everything else that reads `.reticle` here already treats that as
374
- * "no project record" rather than as a failure; the fixtures file is no different, and a throw on
375
- * this path would turn a missing directory into a refused drive.
376
- */
377
- function safeRoot(deps, sessionId) {
378
- try {
379
- return sessionRoot(deps, sessionId);
380
- }
381
- catch {
382
- return undefined;
383
- }
384
- }
385
- function pinned(options) {
452
+ export function pinned(options) {
386
453
  return options.sessionId === undefined ? {} : { sessionId: options.sessionId };
387
454
  }
388
455
  /**
389
- * Pick the driver.
390
- *
391
- * The Anthropic driver stays the default, and an explicit `RETICLE_HARNESS_DRIVER=jev` is the only
392
- * thing that moves off it when both are configured — a driver change is a change in how the app gets
393
- * driven, and inferring one from which key happens to be exported would swap it under people who
394
- * merely linked their account.
395
- *
396
- * The fall-through is the other direction and is not a preference: with no Anthropic key and a
397
- * platform key present, Jev is not the cheaper option, it is the only one. Choosing it there is what
398
- * makes the harness work for somebody who never had a model API key of their own, which is most
399
- * people — and the reason this driver is worth having at all.
456
+ * The driver: the platform's Harness, which decides every step on its side while this machine
457
+ * executes. There is no other: the decisions, the models and the planning run only on the platform.
400
458
  */
401
- function buildDriver(env, maxSteps, plan, requested, fills) {
402
- // The two halves of what `.reticle` knows, handed to the one driver that can use both: what is
403
- // worth doing, and the names the app uses for what it does. The second is what lets a declared
404
- // consequence be SAVED rather than merely proved; see `consequencesFor`.
405
- const fromReticle = {
406
- plan: plan.steps,
407
- vocabulary: plan.vocabulary,
408
- /*
409
- * Generation is offered ONLY when a generating model is already configured, and it stays out of
410
- * the per-turn loop either way: put a text model on every turn and you pay its per-turn tax on
411
- * the 94% of steps that are clicks, which is the entire cost the cheap driver exists to avoid.
412
- * With no such key the label heuristic answers, exactly as it did before this existed.
413
- */
414
- fillValue: fillValues({
415
- secret: (label) => env[secretEnvKey(label)],
416
- ...(fills === undefined ? {} : { cache: fills }),
417
- // GPT first: a linked machine reaches it through the platform with no key of its own.
418
- ...(openAiOptionsFromEnv(env) === undefined
419
- ? {}
420
- : { openai: openAiOptionsFromEnv(env) }),
421
- ...(harnessOptionsFromEnv(env) === undefined
422
- ? {}
423
- : { generator: harnessOptionsFromEnv(env) }),
459
+ export function buildDriver(env, maxSteps, plan, requested, persona,
460
+ /** A goal driven with no persona: it names the drive's flows as a persona would. */
461
+ journey) {
462
+ if (requested !== undefined && SERVER_DRIVER !== requested)
463
+ throw new Error(msgUnknownDriver(requested));
464
+ const platform = serverOptionsFromEnv(env);
465
+ if (platform === undefined)
466
+ throw new Error(MSG_NO_HARNESS_KEY);
467
+ return {
468
+ driver: serverDriver({
469
+ ...platform,
470
+ ...(persona === undefined ? {} : { persona }),
471
+ ...(journey === undefined ? {} : { journey }),
472
+ plan: planAsText(plan),
473
+ planSteps: plan.steps,
474
+ vocabulary: plan.vocabulary,
475
+ maxSteps,
424
476
  }),
477
+ name: SERVER_DRIVER,
425
478
  };
426
- const jev = jevOptionsFromEnv(env);
427
- const anthropic = harnessOptionsFromEnv(env);
428
- const asked = requested;
429
- // A driver that was ASKED for and is not configured is an error, never a substitution. Quietly
430
- // falling back would make every comparison between two drivers a possible lie about which one
431
- // produced the result — and comparing them is the main reason anyone names one.
432
- if (JEV_DRIVER_NAME === asked) {
433
- if (jev === undefined)
434
- throw new Error(MSG_NO_JEV_KEY);
435
- // The Jev driver is told the budget because it has a teardown to reach; the Anthropic driver is
436
- // not, because it calls `finish` itself and being handed a number it did not ask for is how a
437
- // second copy of the budget starts drifting from the loop's.
438
- return { driver: jevDriver({ ...jev, maxSteps, ...fromReticle }), name: JEV_DRIVER_NAME };
439
- }
440
- if (ANTHROPIC_DRIVER_NAME === asked) {
441
- if (anthropic === undefined)
442
- throw new Error(MSG_NO_HARNESS_KEY);
443
- return { driver: harnessDriver(anthropic), name: ANTHROPIC_DRIVER_NAME };
444
- }
445
- if (OPENAI_DRIVER_NAME === asked) {
446
- const openai = openAiOptionsFromEnv(env);
447
- if (openai === undefined)
448
- throw new Error(MSG_NO_OPENAI_KEY);
449
- return { driver: openAiDriver(openai), name: OPENAI_DRIVER_NAME };
450
- }
451
- if (asked !== undefined && 0 < asked.length)
452
- throw new Error(msgUnknownDriver(asked));
453
- if (anthropic !== undefined)
454
- return { driver: harnessDriver(anthropic), name: ANTHROPIC_DRIVER_NAME };
455
- if (jev !== undefined)
456
- return { driver: jevDriver({ ...jev, maxSteps, ...fromReticle }), name: JEV_DRIVER_NAME };
457
- throw new Error(MSG_NO_HARNESS_KEY);
458
479
  }
@@ -74,3 +74,15 @@ export declare function buildHarnessPlan(domain: DomainModel): HarnessPlan;
74
74
  * so it can prefer finishing the cheap deterministic half before spending itself on the rest.
75
75
  */
76
76
  export declare function planAsText(plan: HarnessPlan): string;
77
+ /**
78
+ * The plan a model drives from: the gaps only. Saved flows are replayed by the plan's own lanes, or
79
+ * not at all when a person named the journey; a model handed them replayed every one, unrelated to
80
+ * the journey it was asked for, and spent most of its drive there.
81
+ */
82
+ export declare function withoutReplays(plan: HarnessPlan): HarnessPlan;
83
+ /**
84
+ * What the planner is told about the product: the plan from `.reticle`, then the app's own controls
85
+ * as they are on screen now. With only the plan, an empty project read "PLAN: none" and the planner
86
+ * proposed generic people (an admin, a sign-in journey) for an app that has neither.
87
+ */
88
+ export declare function aboutTheApp(planText: string, invoke: (name: string, args: Record<string, unknown>) => Promise<unknown>, seenBefore?: readonly string[]): Promise<string>;
@@ -21,6 +21,7 @@
21
21
  * Nothing here calls a model. The plan is the deterministic half, and the model's only job is to
22
22
  * choose among steps somebody already decided were worth taking.
23
23
  */
24
+ import { ReticleTool } from '@reticlehq/core';
24
25
  export const PlanStepKind = {
25
26
  /** Already recorded: replay it deterministically instead of paying a model to rediscover it. */
26
27
  REPLAY: 'replay',
@@ -154,3 +155,33 @@ export function planAsText(plan) {
154
155
  const lines = plan.steps.map((step, index) => ` ${String(index + 1)}. [${step.kind}] ${step.target} — ${step.why}`);
155
156
  return `PLAN (from .reticle, not from the source):\n${lines.join('\n')}`;
156
157
  }
158
+ /**
159
+ * The plan a model drives from: the gaps only. Saved flows are replayed by the plan's own lanes, or
160
+ * not at all when a person named the journey; a model handed them replayed every one, unrelated to
161
+ * the journey it was asked for, and spent most of its drive there.
162
+ */
163
+ export function withoutReplays(plan) {
164
+ return { ...plan, steps: plan.steps.filter((step) => PlanStepKind.DRIVE === step.kind) };
165
+ }
166
+ /** How much of the live page the planner reads. Enough for a dashboard's nav and main controls. */
167
+ const MAX_ABOUT_SNAPSHOT = 6_000;
168
+ /**
169
+ * What the planner is told about the product: the plan from `.reticle`, then the app's own controls
170
+ * as they are on screen now. With only the plan, an empty project read "PLAN: none" and the planner
171
+ * proposed generic people (an admin, a sign-in journey) for an app that has neither.
172
+ */
173
+ export async function aboutTheApp(planText, invoke, seenBefore = []) {
174
+ const seen = 0 === seenBefore.length
175
+ ? ''
176
+ : `\n\nCONTROLS SEEN ACROSS THE WHOLE APP LAST TIME, BY PAGE (not only this page):\n${seenBefore.join('\n')}`;
177
+ try {
178
+ const snap = await invoke(ReticleTool.SNAPSHOT, { mode: 'interactive' });
179
+ const tree = 'object' === typeof snap && null !== snap ? snap.tree : undefined;
180
+ if ('string' !== typeof tree || 0 === tree.length)
181
+ return `${planText}${seen}`;
182
+ return `${planText}\n\nTHE APP AS IT IS ON SCREEN NOW (its own controls, in its own words):\n${tree.slice(0, MAX_ABOUT_SNAPSHOT)}${seen}`;
183
+ }
184
+ catch {
185
+ return `${planText}${seen}`;
186
+ }
187
+ }
@@ -0,0 +1,8 @@
1
+ import type { ToolDeps } from './tool-kit.js';
2
+ import { type ExploreOptions, type ExploreResult } from './harness-explore.js';
3
+ /**
4
+ * The platform plans the drive; this machine runs the plan: saved flows replayed in lanes side by side, shared starts
5
+ * driven once and branched from, open journeys handed to the model. `undefined` when there is
6
+ * nothing to plan from, so the caller drives the app with no plan, as before.
7
+ */
8
+ export declare function exploreScript(deps: ToolDeps, env: Record<string, string | undefined>, options: ExploreOptions, before: ReadonlySet<string>): Promise<ExploreResult | undefined>;