@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,123 @@
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 { REDACTED_VALUE, scrubKnownSecrets } from '@reticlehq/core';
6
+ function redactExactValue(text, value) {
7
+ if (0 === value.length)
8
+ return text;
9
+ const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
10
+ return text.replace(new RegExp(`(^|[^a-zA-Z0-9])${escaped}(?=$|[^a-zA-Z0-9])`, 'g'), `$1${REDACTED_VALUE}`);
11
+ }
12
+ /**
13
+ * Scrub known secret shapes and raw seedStorage values (cookies, localStorage, sessionStorage)
14
+ * from an error string before it reaches daemon logs, metrics, or MCP error responses.
15
+ */
16
+ export function scrubSeedFromError(text, seed) {
17
+ let out = scrubKnownSecrets(text);
18
+ if (undefined !== seed && null !== seed && 'object' === typeof seed) {
19
+ const s = seed;
20
+ const cookies = s['cookies'];
21
+ if (undefined !== cookies && null !== cookies) {
22
+ if (Array.isArray(cookies)) {
23
+ for (const c of cookies) {
24
+ if (undefined !== c && null !== c && 'object' === typeof c) {
25
+ const val = c['value'];
26
+ if (undefined !== val && 'string' === typeof val) {
27
+ out = redactExactValue(out, val);
28
+ }
29
+ }
30
+ }
31
+ }
32
+ else if ('object' === typeof cookies) {
33
+ for (const v of Object.values(cookies)) {
34
+ if ('string' === typeof v) {
35
+ out = redactExactValue(out, v);
36
+ }
37
+ }
38
+ }
39
+ }
40
+ const local = s['local'];
41
+ if (undefined !== local && null !== local && 'object' === typeof local) {
42
+ for (const v of Object.values(local)) {
43
+ if ('string' === typeof v) {
44
+ out = redactExactValue(out, v);
45
+ }
46
+ }
47
+ }
48
+ const session = s['session'];
49
+ if (undefined !== session && null !== session && 'object' === typeof session) {
50
+ for (const v of Object.values(session)) {
51
+ if ('string' === typeof v) {
52
+ out = redactExactValue(out, v);
53
+ }
54
+ }
55
+ }
56
+ }
57
+ return out;
58
+ }
59
+ /** True for a Playwright `storageState()` export (`{ origins: [...] }`), not our seed shape. */
60
+ export function looksLikeStorageStateExport(seed) {
61
+ return Array.isArray(seed?.origins);
62
+ }
63
+ /**
64
+ * Turn a raw navigation failure (Playwright's `page.goto: net::ERR_… at <url>\nCall log:…`, often
65
+ * with ANSI codes) into a short, clean reason — so the agent/user sees "is the app running?" instead
66
+ * of an internals-leaking wall of text.
67
+ */
68
+ export function cleanNavError(err, seed) {
69
+ const rawMsg = err instanceof Error ? err.message : String(err);
70
+ const msg = scrubSeedFromError(rawMsg, seed);
71
+ // Strip ANSI color codes (ESC[…m) Playwright emits — built via fromCharCode to keep the
72
+ // control character out of a regex literal (no-control-regex).
73
+ const ansi = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g');
74
+ const firstLine = (msg.split('\n')[0] ?? msg).replace(ansi, '');
75
+ const netCode = /net::[A-Z_]+/.exec(firstLine);
76
+ if (netCode !== null)
77
+ return netCode[0];
78
+ if (/timeout/i.test(firstLine))
79
+ return 'navigation timed out';
80
+ return firstLine
81
+ .replace(/^page\.goto:\s*/, '')
82
+ .replace(/\s+at\s+https?:\/\/\S+.*$/, '')
83
+ .trim()
84
+ .slice(0, 100);
85
+ }
86
+ /**
87
+ * Determine if a required seedStorage application precondition failed.
88
+ *
89
+ * Checks strong signals:
90
+ * 1. HTTP 401/403 status on the initial navigation.
91
+ * 2. Cross-origin redirect when localStorage or sessionStorage was requested (skipped by origin-scoping).
92
+ * 3. Clear redirect to an authentication/login endpoint when the requested URL was not a login page.
93
+ *
94
+ * Normal app redirects (e.g. / → /dashboard) are valid and return undefined.
95
+ */
96
+ export function evaluateSeedPrecondition(requestedUrl, actualUrl, seed, navStatus) {
97
+ if (401 === navStatus || 403 === navStatus) {
98
+ return `HTTP ${navStatus} returned on initial seeded navigation to ${requestedUrl}`;
99
+ }
100
+ if (actualUrl === undefined || '' === actualUrl.trim())
101
+ return undefined;
102
+ let reqParsed;
103
+ let actParsed;
104
+ try {
105
+ reqParsed = new URL(requestedUrl);
106
+ actParsed = new URL(actualUrl);
107
+ }
108
+ catch {
109
+ return undefined;
110
+ }
111
+ // Cross-origin redirect when local or session storage was requested
112
+ const hasStorage = (undefined !== seed.local && 0 < Object.keys(seed.local).length) ||
113
+ (undefined !== seed.session && 0 < Object.keys(seed.session).length);
114
+ if (hasStorage && reqParsed.origin !== actParsed.origin) {
115
+ return `seeded storage precondition not established: cross-origin redirect from ${reqParsed.origin} to ${actParsed.origin} skipped origin-scoped storage injection`;
116
+ }
117
+ // Clear redirect to an authentication/login endpoint when the requested URL was not a login page
118
+ const isAuthPath = (pathname) => /(?:^|\/)(?:login|signin|sign-in|auth\/login|auth\/signin|session\/new)(?:$|\/)/i.test(pathname);
119
+ if (!isAuthPath(reqParsed.pathname) && isAuthPath(actParsed.pathname)) {
120
+ return `seeded authentication precondition not established: redirected from ${reqParsed.pathname} to login page (${actParsed.pathname})`;
121
+ }
122
+ return undefined;
123
+ }
@@ -14,34 +14,14 @@ import { type StorageFixturePort } from '../../portal/input/storage-fixture.js';
14
14
  import { type NetworkMutationPort } from '../../portal/input/network-mutation.js';
15
15
  import type { Perturbation } from '@reticlehq/core';
16
16
  import { type SeedStorage } from '@reticlehq/core';
17
+ import { HudVisibility } from '@reticlehq/core';
17
18
  import type { ToolDef } from './tool-kit.js';
18
19
  /**
19
20
  * Append Reticle identity params (__reticle_session, optional __reticle_project) to a URL so the app's own SDK
20
21
  * adopts them on connect. Pure; falls back to plain concatenation if the URL can't be parsed.
21
22
  */
22
- export declare function appendReticleParams(url: string, session: string, projectId?: string): string;
23
- /**
24
- * Scrub known secret shapes and raw seedStorage values (cookies, localStorage, sessionStorage)
25
- * from an error string before it reaches daemon logs, metrics, or MCP error responses.
26
- */
27
- export declare function scrubSeedFromError(text: string, seed?: unknown): string;
28
- /**
29
- * Turn a raw navigation failure (Playwright's `page.goto: net::ERR_… at <url>\nCall log:…`, often
30
- * with ANSI codes) into a short, clean reason — so the agent/user sees "is the app running?" instead
31
- * of an internals-leaking wall of text.
32
- */
33
- export declare function cleanNavError(err: unknown, seed?: SeedStorage): string;
34
- /**
35
- * Determine if a required seedStorage application precondition failed.
36
- *
37
- * Checks strong signals:
38
- * 1. HTTP 401/403 status on the initial navigation.
39
- * 2. Cross-origin redirect when localStorage or sessionStorage was requested (skipped by origin-scoping).
40
- * 3. Clear redirect to an authentication/login endpoint when the requested URL was not a login page.
41
- *
42
- * Normal app redirects (e.g. / → /dashboard) are valid and return undefined.
43
- */
44
- export declare function evaluateSeedPrecondition(requestedUrl: string, actualUrl: string | undefined, seed: SeedStorage, navStatus?: number): string | undefined;
23
+ export declare function appendReticleParams(url: string, session: string, projectId?: string, hud?: HudVisibility): string;
24
+ export { cleanNavError, evaluateSeedPrecondition, scrubSeedFromError } from './lease-seed.js';
45
25
  /**
46
26
  * The session a lease's tab actually registered as, or undefined while nothing has.
47
27
  *
@@ -20,10 +20,10 @@ import { readProjectFramework, readProjectId } from '../../command/cli/ports/res
20
20
  import { hasAnyAppConnectedBefore } from '../../memory/recall/prior/connection-memory.js';
21
21
  import { AGENT_DRIVING_ELSEWHERE, AGENT_DRIVING_HERE_AGAIN, watchersToNotify, } from '../../portal/session/lease-visibility.js';
22
22
  import { reticleStateHome } from '../../command/daemon/daemon.js';
23
- import { LeaseNotReadyReason, REDACTED_VALUE, RETICLE_URL_PARAM, RETICLE_DEFAULT_PORT, ReticleCommand, SeedStorageSchema, scrubKnownSecrets, } from '@reticlehq/core';
24
- import { ReticleTool, SESSION_HEALTH } from '@reticlehq/core';
23
+ import { LeaseNotReadyReason, RETICLE_URL_PARAM, RETICLE_DEFAULT_PORT, ReticleCommand, SeedStorageSchema, } from '@reticlehq/core';
24
+ import { HudVisibility, ReticleTool, SESSION_HEALTH } from '@reticlehq/core';
25
25
  import { asString } from '@reticlehq/core';
26
- import { chromiumHint } from '../../command/cli/doctor/browser/chromium-hint.js';
26
+ import { chromiumPreflightRefusal } from '../../command/cli/doctor/browser/chromium-hint.js';
27
27
  /**
28
28
  * Everything the daemon already knows about why a leased tab might not have dialled in.
29
29
  *
@@ -95,13 +95,15 @@ function alreadyHeldHint(sessionId, origin) {
95
95
  * Append Reticle identity params (__reticle_session, optional __reticle_project) to a URL so the app's own SDK
96
96
  * adopts them on connect. Pure; falls back to plain concatenation if the URL can't be parsed.
97
97
  */
98
- export function appendReticleParams(url, session, projectId) {
98
+ export function appendReticleParams(url, session, projectId, hud) {
99
99
  try {
100
100
  const u = new URL(url);
101
101
  u.searchParams.set(RETICLE_URL_PARAM.SESSION, session);
102
102
  if (projectId !== undefined && projectId.length > 0) {
103
103
  u.searchParams.set(RETICLE_URL_PARAM.PROJECT, projectId);
104
104
  }
105
+ if (hud !== undefined)
106
+ u.searchParams.set(RETICLE_URL_PARAM.HUD, hud);
105
107
  return u.toString();
106
108
  }
107
109
  catch {
@@ -112,124 +114,8 @@ export function appendReticleParams(url, session, projectId) {
112
114
  return `${url}${sep}${RETICLE_URL_PARAM.SESSION}=${encodeURIComponent(session)}${proj}`;
113
115
  }
114
116
  }
115
- function redactExactValue(text, value) {
116
- if (0 === value.length)
117
- return text;
118
- const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
119
- return text.replace(new RegExp(`(^|[^a-zA-Z0-9])${escaped}(?=$|[^a-zA-Z0-9])`, 'g'), `$1${REDACTED_VALUE}`);
120
- }
121
- /**
122
- * Scrub known secret shapes and raw seedStorage values (cookies, localStorage, sessionStorage)
123
- * from an error string before it reaches daemon logs, metrics, or MCP error responses.
124
- */
125
- export function scrubSeedFromError(text, seed) {
126
- let out = scrubKnownSecrets(text);
127
- if (undefined !== seed && null !== seed && 'object' === typeof seed) {
128
- const s = seed;
129
- const cookies = s['cookies'];
130
- if (undefined !== cookies && null !== cookies) {
131
- if (Array.isArray(cookies)) {
132
- for (const c of cookies) {
133
- if (undefined !== c && null !== c && 'object' === typeof c) {
134
- const val = c['value'];
135
- if (undefined !== val && 'string' === typeof val) {
136
- out = redactExactValue(out, val);
137
- }
138
- }
139
- }
140
- }
141
- else if ('object' === typeof cookies) {
142
- for (const v of Object.values(cookies)) {
143
- if ('string' === typeof v) {
144
- out = redactExactValue(out, v);
145
- }
146
- }
147
- }
148
- }
149
- const local = s['local'];
150
- if (undefined !== local && null !== local && 'object' === typeof local) {
151
- for (const v of Object.values(local)) {
152
- if ('string' === typeof v) {
153
- out = redactExactValue(out, v);
154
- }
155
- }
156
- }
157
- const session = s['session'];
158
- if (undefined !== session && null !== session && 'object' === typeof session) {
159
- for (const v of Object.values(session)) {
160
- if ('string' === typeof v) {
161
- out = redactExactValue(out, v);
162
- }
163
- }
164
- }
165
- }
166
- return out;
167
- }
168
- /** True for a Playwright `storageState()` export (`{ origins: [...] }`), not our seed shape. */
169
- function looksLikeStorageStateExport(seed) {
170
- return Array.isArray(seed?.origins);
171
- }
172
- /**
173
- * Turn a raw navigation failure (Playwright's `page.goto: net::ERR_… at <url>\nCall log:…`, often
174
- * with ANSI codes) into a short, clean reason — so the agent/user sees "is the app running?" instead
175
- * of an internals-leaking wall of text.
176
- */
177
- export function cleanNavError(err, seed) {
178
- const rawMsg = err instanceof Error ? err.message : String(err);
179
- const msg = scrubSeedFromError(rawMsg, seed);
180
- // Strip ANSI color codes (ESC[…m) Playwright emits — built via fromCharCode to keep the
181
- // control character out of a regex literal (no-control-regex).
182
- const ansi = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g');
183
- const firstLine = (msg.split('\n')[0] ?? msg).replace(ansi, '');
184
- const netCode = /net::[A-Z_]+/.exec(firstLine);
185
- if (netCode !== null)
186
- return netCode[0];
187
- if (/timeout/i.test(firstLine))
188
- return 'navigation timed out';
189
- return firstLine
190
- .replace(/^page\.goto:\s*/, '')
191
- .replace(/\s+at\s+https?:\/\/\S+.*$/, '')
192
- .trim()
193
- .slice(0, 100);
194
- }
195
- /**
196
- * Determine if a required seedStorage application precondition failed.
197
- *
198
- * Checks strong signals:
199
- * 1. HTTP 401/403 status on the initial navigation.
200
- * 2. Cross-origin redirect when localStorage or sessionStorage was requested (skipped by origin-scoping).
201
- * 3. Clear redirect to an authentication/login endpoint when the requested URL was not a login page.
202
- *
203
- * Normal app redirects (e.g. / → /dashboard) are valid and return undefined.
204
- */
205
- export function evaluateSeedPrecondition(requestedUrl, actualUrl, seed, navStatus) {
206
- if (401 === navStatus || 403 === navStatus) {
207
- return `HTTP ${navStatus} returned on initial seeded navigation to ${requestedUrl}`;
208
- }
209
- if (actualUrl === undefined || '' === actualUrl.trim())
210
- return undefined;
211
- let reqParsed;
212
- let actParsed;
213
- try {
214
- reqParsed = new URL(requestedUrl);
215
- actParsed = new URL(actualUrl);
216
- }
217
- catch {
218
- return undefined;
219
- }
220
- // Cross-origin redirect when local or session storage was requested
221
- const hasStorage = (undefined !== seed.local && 0 < Object.keys(seed.local).length) ||
222
- (undefined !== seed.session && 0 < Object.keys(seed.session).length);
223
- if (hasStorage && reqParsed.origin !== actParsed.origin) {
224
- return `seeded storage precondition not established: cross-origin redirect from ${reqParsed.origin} to ${actParsed.origin} skipped origin-scoped storage injection`;
225
- }
226
- // Clear redirect to an authentication/login endpoint when the requested URL was not a login page
227
- const isAuthPath = (pathname) => /(?:^|\/)(?:login|signin|sign-in|auth\/login|auth\/signin|session\/new)(?:$|\/)/i.test(pathname);
228
- if (!isAuthPath(reqParsed.pathname) && isAuthPath(actParsed.pathname)) {
229
- return `seeded authentication precondition not established: redirected from ${reqParsed.pathname} to login page (${actParsed.pathname})`;
230
- }
231
- return undefined;
232
- }
117
+ export { cleanNavError, evaluateSeedPrecondition, scrubSeedFromError } from './lease-seed.js';
118
+ import { cleanNavError, evaluateSeedPrecondition, looksLikeStorageStateExport, scrubSeedFromError, } from './lease-seed.js';
233
119
  /** A fresh, collision-resistant lease id. Uses crypto at the I/O boundary (not pure logic). */
234
120
  function newLeaseId() {
235
121
  const uuid = 'function' === typeof globalThis.crypto?.randomUUID
@@ -437,7 +323,15 @@ export const LEASE_ACQUIRE_TOOL = {
437
323
  .string()
438
324
  .optional()
439
325
  .describe('Stable project id to stamp on the leased tab so the agent can scope to it.'),
440
- seedStorage: SeedStorageSchema.optional().describe('Initial storage state (localStorage, sessionStorage, cookies) to seed before the first navigation (e.g. to start already authenticated).'),
326
+ seedStorage: SeedStorageSchema.optional().describe('Initial storage state (local, session and cookies) to seed before the first navigation (e.g. `seedStorage: { local: { token: "..." } }` to start already authenticated).'),
327
+ headed: z
328
+ .boolean()
329
+ .optional()
330
+ .describe('Open it in a browser window somebody can watch, instead of headless.'),
331
+ hud: z
332
+ .enum([HudVisibility.SHOWN, HudVisibility.HIDDEN, HudVisibility.REMOVED])
333
+ .optional()
334
+ .describe('How the HUD starts on the page: shown (default), hidden, or removed.'),
441
335
  },
442
336
  outputSchema: {
443
337
  sessionId: z.string(),
@@ -487,12 +381,13 @@ export const LEASE_ACQUIRE_TOOL = {
487
381
  // fine. Say the real thing at the first refusal instead. The phrasing carries "Chromium is not
488
382
  // installed" so error-recovery routes it to the NO_POOL fix (install + drive a human tab).
489
383
  if (deps.browserProbe !== undefined) {
490
- const probe = await deps.browserProbe();
491
- if (!probe.exists) {
492
- throw new Error(`Chromium is not installed for Playwright — ${chromiumHint(probe)}`);
493
- }
384
+ const refusal = chromiumPreflightRefusal(await deps.browserProbe());
385
+ if (refusal !== undefined)
386
+ throw new Error(refusal);
494
387
  }
495
388
  const projectId = asString(args['projectId']);
389
+ const headed = true === args['headed'];
390
+ const hud = Object.values(HudVisibility).find((v) => v === args['hud']);
496
391
  const seedStorageArg = args['seedStorage'];
497
392
  let validatedSeed;
498
393
  if (seedStorageArg !== undefined) {
@@ -526,10 +421,12 @@ export const LEASE_ACQUIRE_TOOL = {
526
421
  await priorLock.catch(() => undefined);
527
422
  }
528
423
  try {
529
- const existing = origin === undefined ? undefined : pool.leaseIdOnOrigin?.(origin);
424
+ // Only a lease THIS caller took: another agent's tab is never reused or released (#1226).
425
+ // A window asked for is never answered with the headless tab on that origin, nor the reverse.
426
+ const held = origin === undefined ? undefined : pool.leaseIdOnOrigin(origin, deps.attachId);
427
+ const existing = held !== undefined && pool.isHeaded(held) === headed ? held : undefined;
530
428
  if (validatedSeed !== undefined && existing !== undefined) {
531
- // Caller explicitly requested storage seeding: do not reuse an existing context on this origin,
532
- // release it so the new lease starts clean with the caller-provided state.
429
+ // Seeding asked for: release its own context so the new lease starts with the given state.
533
430
  await pool.release(existing);
534
431
  }
535
432
  else if (existing !== undefined && origin !== undefined) {
@@ -574,11 +471,13 @@ export const LEASE_ACQUIRE_TOOL = {
574
471
  await pool.release(existing);
575
472
  }
576
473
  const sessionId = newLeaseId();
577
- const navUrl = appendReticleParams(url, sessionId, projectId);
474
+ const navUrl = appendReticleParams(url, sessionId, projectId, hud);
578
475
  let lease;
579
476
  try {
580
477
  lease = await pool.acquire(navUrl, {
581
478
  sessionId,
479
+ ...(headed ? { headed } : {}),
480
+ ...(deps.attachId === undefined ? {} : { owner: deps.attachId }),
582
481
  ...(validatedSeed !== undefined ? { seedStorage: validatedSeed } : {}),
583
482
  });
584
483
  }
@@ -80,8 +80,8 @@ const NO_TOOL_EQUIVALENT = [
80
80
  * every session, and the longer two-route version cost 67 bytes there against a budget measured
81
81
  * in whole-session terms — for advice that is read once, on a first run.
82
82
  *
83
- * `explore` needs ANTHROPIC_API_KEY and says so itself when it is missing, naming the fallback in
84
- * the same breath ("flows are recorded by your own coding agent through the MCP tools"). That
83
+ * `explore` runs on the platform and says so itself when it is not linked, naming the fallback in
84
+ * the same breath (drive the journey yourself with the MCP tools). That
85
85
  * error is now recognised as ours, so the reader who hits it gets the next step instead of an
86
86
  * invitation to file a defect report. One hop, and every hop is honest.
87
87
  */
@@ -0,0 +1,19 @@
1
+ export declare const NextText: {
2
+ readonly DECLARE: "Record what the user asked, once per task, verbatim: reticle_run { tool: \"reticle_intent\", args: { action: \"declare\", request: \"<their words>\" } }. It travels with every run.";
3
+ readonly SYNC_PROBLEM: (why: string) => string;
4
+ readonly CONNECT: "These runs stay on this machine. To share them on the team dashboard, the user runs once: npx @reticlehq/server connect";
5
+ readonly FINISH_LINKED: "Done driving? reticle_session { action: \"yield\" } hands the tab back; this run syncs to the platform on its own.";
6
+ readonly FINISH: "Done driving? reticle_session { action: \"yield\" } hands the tab back.";
7
+ readonly REPLAY: (flows: number) => string;
8
+ };
9
+ /** Tests only. */
10
+ export declare function resetNextStep(): void;
11
+ export interface NextStepInput {
12
+ tool: string;
13
+ /** Whether this result carries a verdict. */
14
+ verdict: boolean;
15
+ root: string | undefined;
16
+ now: number;
17
+ }
18
+ /** The line, or undefined when there is nothing the agent should be told right now. */
19
+ export declare function nextStep(input: NextStepInput): string | undefined;
@@ -0,0 +1,159 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { ReticleDir, ReticleEnv } from '@reticlehq/core';
4
+ /**
5
+ * The one thing the agent should do next, on the result of the call it just made.
6
+ *
7
+ * Every client reads tool results; few read a briefing twice, and none keep a skill in mind across a
8
+ * long session. So what an agent must not forget (record what the user asked, hand the tab back,
9
+ * notice when its runs are not reaching the platform, tell the user how to connect) is said where
10
+ * the agent is already looking, one short line, on the moment it applies. It is placed first in the
11
+ * result, because a model that truncates a long result keeps its beginning.
12
+ *
13
+ * The daemon syncs runs itself, whichever agent drove them; nothing here asks an agent to push.
14
+ * It only says when the sync needs a person (a refusal, a failure, a project never connected).
15
+ */
16
+ /** How long a declared request counts as "the current task". Matches the run store's window. */
17
+ const REQUEST_FRESH_MS = 6 * 60 * 60 * 1000;
18
+ /** A reminder that is not about a verdict is repeated at most this often, per project. */
19
+ const QUIET_CALLS = 10;
20
+ /** An unlinked project hears how to connect on every Nth verdict, the first included. */
21
+ const CONNECT_EVERY = 5;
22
+ /**
23
+ * How often the agent is asked to record the request before the other lines get their turn. In
24
+ * every recorded run the agent ignored it, and because it always won, no agent was ever told its
25
+ * runs were not reaching the platform.
26
+ */
27
+ const DECLARE_ASKS = 2;
28
+ /** A run file this much newer than the last push, and this old, was not sent. */
29
+ const UNSENT_AFTER_MS = 2 * 60 * 1000;
30
+ export const NextText = {
31
+ DECLARE: 'Record what the user asked, once per task, verbatim: reticle_run { tool: "reticle_intent", args: { action: "declare", request: "<their words>" } }. It travels with every run.',
32
+ SYNC_PROBLEM: (why) => `Runs are not reaching the platform (${why}). reticle_run { tool: "reticle_project", args: { push: true } } retries and reports why.`,
33
+ CONNECT: 'These runs stay on this machine. To share them on the team dashboard, the user runs once: npx @reticlehq/server connect',
34
+ FINISH_LINKED: 'Done driving? reticle_session { action: "yield" } hands the tab back; this run syncs to the platform on its own.',
35
+ FINISH: 'Done driving? reticle_session { action: "yield" } hands the tab back.',
36
+ REPLAY: (flows) => `This app has ${String(flows)} saved flow(s). Replay them before driving anything by hand: reticle_verify { action: "flows" } re-verifies them with no model.`,
37
+ };
38
+ const roots = new Map();
39
+ /** Tests only. */
40
+ export function resetNextStep() {
41
+ roots.clear();
42
+ }
43
+ function readJson(path) {
44
+ try {
45
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
46
+ return 'object' === typeof parsed && null !== parsed
47
+ ? parsed
48
+ : undefined;
49
+ }
50
+ catch {
51
+ return undefined;
52
+ }
53
+ }
54
+ /** Declared within the window, read from the file's own stamp (or its age when it has none). */
55
+ function requestDeclared(root, now) {
56
+ const path = join(root, ReticleDir.REQUEST_FILE);
57
+ if (!existsSync(path))
58
+ return false;
59
+ const at = readJson(path)?.['at'];
60
+ const when = 'number' === typeof at ? at : statSync(path).mtimeMs;
61
+ return now - when < REQUEST_FRESH_MS;
62
+ }
63
+ /** Run files written after the last push and old enough that a sync cycle should have sent them. */
64
+ function unsentRuns(root, lastPushAt, now) {
65
+ try {
66
+ const dir = join(root, ReticleDir.RUNS_SUBDIR);
67
+ return readdirSync(dir).filter((name) => {
68
+ const at = statSync(join(dir, name)).mtimeMs;
69
+ return at > lastPushAt + UNSENT_AFTER_MS && now - at > UNSENT_AFTER_MS;
70
+ }).length;
71
+ }
72
+ catch {
73
+ return 0;
74
+ }
75
+ }
76
+ /**
77
+ * Why sync needs a person, from the sync's own bookkeeping; undefined when it is fine.
78
+ *
79
+ * Runs that were written and never sent count too. A project the sync loop never looked at has no
80
+ * error and no refusal, which is how a whole day of runs stayed on one machine with every check
81
+ * reporting fine.
82
+ */
83
+ function syncProblem(root, now) {
84
+ const state = readJson(join(root, ReticleDir.CLOUD_STATE_FILE));
85
+ const pushed = state?.['lastPushAt'];
86
+ const unsent = unsentRuns(root, 'number' === typeof pushed ? pushed : 0, now);
87
+ if (0 < unsent)
88
+ return `${String(unsent)} run(s) written here were never sent`;
89
+ if (state === undefined)
90
+ return undefined;
91
+ const error = state['lastError'];
92
+ if ('string' === typeof error && 0 < error.length)
93
+ return error.slice(0, 120);
94
+ const refused = state['refusedRuns'];
95
+ const count = 'object' === typeof refused && null !== refused ? Object.keys(refused).length : 0;
96
+ return 0 < count ? `${String(count)} run(s) refused` : undefined;
97
+ }
98
+ /** Saved flow files under `flows/`, one level of project folders deep. */
99
+ function savedFlows(dir) {
100
+ try {
101
+ return readdirSync(dir, { withFileTypes: true }).reduce((n, entry) => n +
102
+ (entry.isDirectory()
103
+ ? readdirSync(join(dir, entry.name)).filter((f) => f.endsWith('.json')).length
104
+ : entry.name.endsWith('.json')
105
+ ? 1
106
+ : 0), 0);
107
+ }
108
+ catch {
109
+ return 0;
110
+ }
111
+ }
112
+ /** The line, or undefined when there is nothing the agent should be told right now. */
113
+ export function nextStep(input) {
114
+ const { root } = input;
115
+ if (root === undefined)
116
+ return undefined;
117
+ const state = roots.get(root) ?? {
118
+ calls: 0,
119
+ verdicts: 0,
120
+ lastQuietAt: Number.NEGATIVE_INFINITY,
121
+ declareAsks: 0,
122
+ replayTold: false,
123
+ };
124
+ roots.set(root, state);
125
+ state.calls += 1;
126
+ if (input.verdict)
127
+ state.verdicts += 1;
128
+ const linked = existsSync(join(root, ReticleDir.CLOUD_LINK_FILE));
129
+ // A key in the environment syncs too, so its runs not arriving is just as much a problem.
130
+ const sends = linked || 0 < (process.env[ReticleEnv.API_KEY] ?? '').length;
131
+ const quietDue = state.calls - state.lastQuietAt >= QUIET_CALLS;
132
+ // Measured on a whole-app drive: an agent re-drove everything by hand while saved flows sat
133
+ // unreplayed, though the skill says to replay first. Said on the first call, where it is in time.
134
+ if (!state.replayTold && !input.tool.endsWith('verify')) {
135
+ state.replayTold = true;
136
+ const saved = savedFlows(join(root, ReticleDir.FLOWS_SUBDIR));
137
+ if (0 < saved)
138
+ return NextText.REPLAY(saved);
139
+ }
140
+ const problem = sends ? syncProblem(root, input.now) : undefined;
141
+ if (problem !== undefined && (input.verdict || quietDue)) {
142
+ state.lastQuietAt = state.calls;
143
+ return NextText.SYNC_PROBLEM(problem);
144
+ }
145
+ // Not on the declare call itself, which is the answer to this line.
146
+ if (!input.tool.endsWith('intent') &&
147
+ state.declareAsks < DECLARE_ASKS &&
148
+ !requestDeclared(root, input.now) &&
149
+ (input.verdict || quietDue)) {
150
+ state.lastQuietAt = state.calls;
151
+ state.declareAsks += 1;
152
+ return NextText.DECLARE;
153
+ }
154
+ if (!input.verdict)
155
+ return undefined;
156
+ if (!sends && 1 === state.verdicts % CONNECT_EVERY)
157
+ return NextText.CONNECT;
158
+ return sends ? NextText.FINISH_LINKED : NextText.FINISH;
159
+ }
@@ -258,7 +258,7 @@ export const OBSERVE_TOOLS = [
258
258
  if (bodyRefusal !== undefined)
259
259
  throw new Error(bodyRefusal);
260
260
  // Honesty: explicit since wins; else default to the last act's cursor; else the whole buffer.
261
- let since = asNumber(args['since']) ?? session.lastAct.cursor() ?? 0;
261
+ const since = asNumber(args['since']) ?? session.lastAct.cursor() ?? 0;
262
262
  // See the note on the assert handler below: a wait cut off by a full-document navigation is
263
263
  // followed to the document that took over, rather than graded as a lost observation there.
264
264
  const predicateStarted = session.elapsed();
@@ -270,8 +270,6 @@ export const OBSERVE_TOOLS = [
270
270
  predicateStarted,
271
271
  reevaluate: (next, budget) => waitForPredicate(next, predicate, budget, 0),
272
272
  });
273
- if (followed.followed)
274
- since = 0;
275
273
  session = followed.session;
276
274
  const verdict = followed.verdict;
277
275
  // match reticle_assert — wrap with control + session health (throttle matters most while blocking)
@@ -452,7 +450,7 @@ export const OBSERVE_TOOLS = [
452
450
  // explicit `since` may reach back past the last step, where replay would not find it. The
453
451
  // store itself refuses when a navigation came after that step: see RecordingStore.markNavigated.
454
452
  if (Verified.YES === decision['verified'] && args['since'] === undefined) {
455
- captureAssertion(deps.recordings, predicate);
453
+ captureAssertion(deps.recordings, predicate, session.id);
456
454
  }
457
455
  return withControl(session, {
458
456
  ...decision,
@@ -0,0 +1,12 @@
1
+ import type { ToolDeps } from './tool-kit.js';
2
+ /** Verified runs kept only on this machine before the platform is worth a sentence. */
3
+ export declare const LOCAL_RUNS_MOMENT = 5;
4
+ /** A `.reticle` this large is history worth keeping somewhere other than one laptop. */
5
+ export declare const LARGE_RETICLE_BYTES: number;
6
+ export declare function takePlatformMoment(deps: Pick<ToolDeps, 'fs' | 'linkedCloud'> & {
7
+ now?: () => number;
8
+ }, raw: Record<string, unknown>,
9
+ /** The `.reticle` of the project the verdict was about — the session's, not the daemon's. */
10
+ rootOf: () => string): Promise<string | undefined>;
11
+ /** Tests only. */
12
+ export declare function resetPlatformMoments(): void;