@reticlehq/server 3.2.0 → 3.4.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 (437) hide show
  1. package/LICENSE-ENTERPRISE +1 -1
  2. package/README.md +3 -3
  3. package/SKILL.md +23 -22
  4. package/dist/chromium-launch-options.d.ts +4 -3
  5. package/dist/chromium-launch-options.js +6 -2
  6. package/dist/command/cli/adhoc-suite.d.ts +41 -0
  7. package/dist/command/cli/adhoc-suite.js +159 -0
  8. package/dist/command/cli/adhoc-verdict.d.ts +58 -2
  9. package/dist/command/cli/adhoc-verdict.js +108 -13
  10. package/dist/command/cli/answers/gate-hook-message.d.ts +6 -0
  11. package/dist/command/cli/answers/gate-hook-message.js +6 -0
  12. package/dist/command/cli/auth/cloud-keystore.d.ts +0 -2
  13. package/dist/command/cli/auth/cloud-keystore.js +0 -3
  14. package/dist/command/cli/auth/cloud-session.d.ts +4 -0
  15. package/dist/command/cli/auth/cloud-session.js +2 -0
  16. package/dist/command/cli/cli-doctor.js +30 -8
  17. package/dist/command/cli/cli-flow-commands.d.ts +3 -6
  18. package/dist/command/cli/cli-flow-commands.js +67 -4
  19. package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
  20. package/dist/command/cli/cli-parse-grammar.js +7 -0
  21. package/dist/command/cli/cli-parse-verify.d.ts +2 -5
  22. package/dist/command/cli/cli-parse-verify.js +86 -13
  23. package/dist/command/cli/cli-parse.d.ts +14 -1
  24. package/dist/command/cli/cli-parse.js +54 -8
  25. package/dist/command/cli/cli-update-commands.js +8 -1
  26. package/dist/command/cli/cli-verify.d.ts +80 -0
  27. package/dist/command/cli/cli-verify.js +181 -26
  28. package/dist/command/cli/cloud-cli.js +5 -2
  29. package/dist/command/cli/cloud-kit.d.ts +0 -2
  30. package/dist/command/cli/cloud-kit.js +3 -5
  31. package/dist/command/cli/cloud-login.js +21 -3
  32. package/dist/command/cli/demo/demo-page.js +6 -3
  33. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +5 -0
  34. package/dist/command/cli/doctor/browser/chromium-hint.js +5 -0
  35. package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
  36. package/dist/command/cli/launch/cli-launch.d.ts +9 -29
  37. package/dist/command/cli/launch/cli-launch.js +27 -49
  38. package/dist/command/cli/launch/sdk-version-match.d.ts +84 -0
  39. package/dist/command/cli/launch/sdk-version-match.js +206 -0
  40. package/dist/command/cli/lifecycle/daemon-lifecycle.js +2 -16
  41. package/dist/command/cli/ports/resolve/cli-port.d.ts +38 -0
  42. package/dist/command/cli/ports/resolve/cli-port.js +169 -15
  43. package/dist/command/cli/report-command.d.ts +16 -0
  44. package/dist/command/cli/report-command.js +97 -0
  45. package/dist/command/cli/setup-mcp-cli.js +13 -13
  46. package/dist/command/cli/tutorial.d.ts +2 -2
  47. package/dist/command/cli/tutorial.js +21 -7
  48. package/dist/command/cli.js +67 -10
  49. package/dist/command/daemon/binding/daemon-status-probe.d.ts +44 -2
  50. package/dist/command/daemon/binding/daemon-status-probe.js +79 -6
  51. package/dist/command/daemon/binding/port-presence.d.ts +1 -1
  52. package/dist/command/daemon/daemon-resolve.d.ts +9 -0
  53. package/dist/command/daemon/daemon-resolve.js +1 -1
  54. package/dist/command/daemon/idle-shutdown.d.ts +2 -1
  55. package/dist/command/daemon/lifetime/daemon-usefulness.d.ts +7 -1
  56. package/dist/command/daemon/lifetime/daemon-usefulness.js +30 -7
  57. package/dist/command/dev/stale-issue-guard.d.ts +3 -4
  58. package/dist/command/dev/stale-issue-guard.js +8 -8
  59. package/dist/command/setup/bringup/connect-lease.d.ts +17 -0
  60. package/dist/command/setup/bringup/connect-lease.js +38 -0
  61. package/dist/command/setup/bringup/dev-server-files.d.ts +68 -0
  62. package/dist/command/setup/bringup/dev-server-files.js +131 -0
  63. package/dist/command/setup/bringup/dev-server-log-cap.d.ts +35 -0
  64. package/dist/command/setup/bringup/dev-server-log-cap.js +121 -0
  65. package/dist/command/setup/bringup/ensure-daemon.d.ts +65 -4
  66. package/dist/command/setup/bringup/ensure-daemon.js +135 -16
  67. package/dist/command/setup/bringup/static-page-server.d.ts +35 -0
  68. package/dist/command/setup/bringup/static-page-server.js +151 -0
  69. package/dist/command/setup/desktop-shape.d.ts +4 -0
  70. package/dist/command/setup/desktop-shape.js +18 -2
  71. package/dist/command/setup/init/init-host.js +8 -0
  72. package/dist/command/setup/init/init-port.d.ts +15 -0
  73. package/dist/command/setup/init/init-port.js +44 -0
  74. package/dist/command/setup/init/init-runtime.d.ts +1 -1
  75. package/dist/command/setup/init/init-runtime.js +39 -2
  76. package/dist/command/setup/node-effects.d.ts +27 -14
  77. package/dist/command/setup/node-effects.js +80 -44
  78. package/dist/command/setup/probe/loopback-probe-urls.d.ts +7 -0
  79. package/dist/command/setup/probe/loopback-probe-urls.js +40 -0
  80. package/dist/command/setup/probe/page-probe.d.ts +13 -2
  81. package/dist/command/setup/probe/page-probe.js +18 -3
  82. package/dist/command/setup/run-setup.d.ts +26 -1
  83. package/dist/command/setup/run-setup.js +96 -13
  84. package/dist/command/setup/session-pick.d.ts +13 -0
  85. package/dist/command/setup/session-pick.js +20 -0
  86. package/dist/command/setup/setup-command.d.ts +2 -14
  87. package/dist/command/setup/setup-command.js +105 -31
  88. package/dist/command/setup/setup-mcp.js +3 -3
  89. package/dist/command/setup/terminal/wait-progress.d.ts +8 -0
  90. package/dist/command/setup/terminal/wait-progress.js +17 -0
  91. package/dist/features/crawl/crawl-empty.js +1 -1
  92. package/dist/features/crawl/crawl-tools.d.ts +6 -0
  93. package/dist/features/crawl/crawl-tools.js +57 -0
  94. package/dist/features/crawl/crawl.d.ts +2 -0
  95. package/dist/features/crawl/crawl.js +171 -156
  96. package/dist/features/exhaust/app-coverage.d.ts +33 -0
  97. package/dist/features/exhaust/app-coverage.js +31 -0
  98. package/dist/features/exhaust/code-coverage.d.ts +47 -0
  99. package/dist/features/exhaust/code-coverage.js +77 -0
  100. package/dist/features/exhaust/exhaust-run.d.ts +33 -0
  101. package/dist/features/exhaust/exhaust-run.js +82 -0
  102. package/dist/features/exhaust/explorer.d.ts +103 -0
  103. package/dist/features/exhaust/explorer.js +195 -0
  104. package/dist/features/exhaust/failure-branches.d.ts +33 -0
  105. package/dist/features/exhaust/failure-branches.js +92 -0
  106. package/dist/features/exhaust/ledger.d.ts +202 -0
  107. package/dist/features/exhaust/ledger.js +252 -0
  108. package/dist/features/exhaust/session-fold.d.ts +31 -0
  109. package/dist/features/exhaust/session-fold.js +79 -0
  110. package/dist/features/exhaust/session-port.d.ts +36 -0
  111. package/dist/features/exhaust/session-port.js +102 -0
  112. package/dist/features/harness/drive-report.d.ts +0 -9
  113. package/dist/features/harness/drive-report.js +18 -0
  114. package/dist/features/harness/driver.js +4 -7
  115. package/dist/features/harness/fill-values.d.ts +6 -0
  116. package/dist/features/harness/fill-values.js +3 -0
  117. package/dist/features/harness/jev-driver.js +36 -22
  118. package/dist/features/harness/openai-driver.js +4 -7
  119. package/dist/features/harness/platform-config.d.ts +10 -0
  120. package/dist/features/harness/platform-config.js +13 -6
  121. package/dist/index.d.ts +10 -1
  122. package/dist/index.js +78 -104
  123. package/dist/judgement/capsule/capsule-store.d.ts +31 -2
  124. package/dist/judgement/capsule/capsule-store.js +58 -6
  125. package/dist/judgement/capsule/capsule.js +5 -0
  126. package/dist/judgement/capsule/causal-summary.d.ts +13 -1
  127. package/dist/judgement/capsule/causal-summary.js +7 -0
  128. package/dist/judgement/domain/domain-model.js +40 -9
  129. package/dist/judgement/oracles/self-instrument.js +4 -2
  130. package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
  131. package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
  132. package/dist/judgement/runs/artifact/gap-report.js +46 -0
  133. package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
  134. package/dist/judgement/runs/artifact/gap-summary.js +85 -0
  135. package/dist/judgement/runs/artifact/render-report.js +4 -1
  136. package/dist/judgement/runs/artifact/replay-mapping.d.ts +2 -2
  137. package/dist/judgement/runs/artifact/replay-mapping.js +44 -9
  138. package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
  139. package/dist/judgement/runs/artifact/to-artifact.js +1 -1
  140. package/dist/judgement/runs/context-tools.js +21 -3
  141. package/dist/judgement/runs/drive-run.js +7 -2
  142. package/dist/judgement/runs/verification-sync.d.ts +3 -1
  143. package/dist/judgement/runs/verification-sync.js +1 -1
  144. package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
  145. package/dist/language/flows/annotate-notes/annotate.js +66 -18
  146. package/dist/language/flows/change/affected.d.ts +9 -0
  147. package/dist/language/flows/change/affected.js +35 -0
  148. package/dist/language/flows/change/gate.d.ts +20 -0
  149. package/dist/language/flows/change/gate.js +12 -1
  150. package/dist/language/flows/flow-anchor.d.ts +12 -0
  151. package/dist/language/flows/flow-anchor.js +20 -0
  152. package/dist/language/flows/flow-author.d.ts +7 -0
  153. package/dist/language/flows/flow-author.js +19 -0
  154. package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
  155. package/dist/language/flows/flow-expect-grammar.js +92 -11
  156. package/dist/language/flows/flow-intent.js +1 -1
  157. package/dist/language/flows/flow-journey.d.ts +54 -0
  158. package/dist/language/flows/flow-journey.js +119 -0
  159. package/dist/language/flows/flow-learning.d.ts +3 -0
  160. package/dist/language/flows/flow-learning.js +39 -8
  161. package/dist/language/flows/flow-replay-run.d.ts +33 -12
  162. package/dist/language/flows/flow-replay-run.js +131 -52
  163. package/dist/language/flows/flow-replay-types.d.ts +9 -0
  164. package/dist/language/flows/flow-replay.d.ts +27 -12
  165. package/dist/language/flows/flow-replay.js +127 -111
  166. package/dist/language/flows/flow-result.d.ts +2 -0
  167. package/dist/language/flows/flow-result.js +25 -0
  168. package/dist/language/flows/flow-step-runners.d.ts +26 -10
  169. package/dist/language/flows/flow-step-runners.js +20 -4
  170. package/dist/language/flows/flow-success.d.ts +21 -6
  171. package/dist/language/flows/flow-success.js +50 -138
  172. package/dist/language/flows/flow-tools.js +17 -5
  173. package/dist/language/flows/flows.d.ts +18 -5
  174. package/dist/language/flows/flows.js +101 -16
  175. package/dist/language/flows/heal.d.ts +9 -1
  176. package/dist/language/flows/heal.js +24 -2
  177. package/dist/language/flows/learned-sources.d.ts +21 -0
  178. package/dist/language/flows/learned-sources.js +84 -0
  179. package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
  180. package/dist/language/flows/recording/recording-backtrack.js +1 -1
  181. package/dist/language/flows/recording/replay-halt.d.ts +2 -1
  182. package/dist/language/flows/recording/replay-halt.js +3 -1
  183. package/dist/language/flows/recording/tape/recordings.d.ts +31 -2
  184. package/dist/language/flows/recording/tape/recordings.js +55 -1
  185. package/dist/language/flows/replay.d.ts +19 -0
  186. package/dist/language/flows/replay.js +53 -13
  187. package/dist/language/flows/stores/annotation-store.d.ts +5 -5
  188. package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
  189. package/dist/language/flows/stores/flake-store.js +2 -3
  190. package/dist/language/flows/suite/coverage.d.ts +1 -1
  191. package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
  192. package/dist/language/flows/verify-change-tools.js +19 -2
  193. package/dist/launch-chromium.d.ts +80 -0
  194. package/dist/launch-chromium.js +218 -0
  195. package/dist/memory/cloud/cloud-config.d.ts +11 -2
  196. package/dist/memory/cloud/cloud-config.js +47 -7
  197. package/dist/memory/cloud/cloud-sync.d.ts +4 -3
  198. package/dist/memory/cloud/cloud-sync.js +7 -10
  199. package/dist/memory/cloud/harness-config.d.ts +2 -0
  200. package/dist/memory/cloud/harness-offer.js +5 -6
  201. package/dist/memory/cloud/harness-switch.js +5 -6
  202. package/dist/memory/cloud/sync-cycle.d.ts +78 -1
  203. package/dist/memory/cloud/sync-cycle.js +321 -53
  204. package/dist/memory/cloud/sync-daemon.d.ts +13 -0
  205. package/dist/memory/cloud/sync-daemon.js +45 -20
  206. package/dist/memory/cloud/sync-disk.js +27 -8
  207. package/dist/memory/impact/impact-store.js +6 -0
  208. package/dist/memory/intent/inline-intent.js +8 -3
  209. package/dist/memory/intent/intent-shard-store.d.ts +33 -47
  210. package/dist/memory/intent/intent-shard-store.js +252 -140
  211. package/dist/memory/intent/intent-shard.d.ts +0 -23
  212. package/dist/memory/intent/intent-shard.js +1 -11
  213. package/dist/memory/intent/intent-store.d.ts +13 -8
  214. package/dist/memory/intent/intent-store.js +46 -90
  215. package/dist/memory/intent/intent-tools.js +34 -23
  216. package/dist/memory/journal/ambient-store.js +2 -3
  217. package/dist/memory/journal/deviation-report.js +4 -1
  218. package/dist/memory/journal/deviation-service.js +10 -2
  219. package/dist/memory/journal/drive-flow.d.ts +19 -14
  220. package/dist/memory/journal/drive-flow.js +65 -48
  221. package/dist/memory/journal/envelope-store.js +2 -3
  222. package/dist/memory/journal/journal-recorder.d.ts +21 -0
  223. package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
  224. package/dist/memory/journal/on-disk/retain-policy.js +44 -0
  225. package/dist/memory/journal/on-disk/retention.d.ts +14 -10
  226. package/dist/memory/journal/on-disk/retention.js +13 -12
  227. package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
  228. package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
  229. package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -11
  230. package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -45
  231. package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
  232. package/dist/memory/journal/on-disk/workspace-tiers.js +90 -0
  233. package/dist/memory/journal/route-template.d.ts +22 -0
  234. package/dist/memory/journal/route-template.js +44 -0
  235. package/dist/memory/journal/session-end.d.ts +13 -2
  236. package/dist/memory/journal/session-end.js +88 -18
  237. package/dist/memory/journal/session-journal.d.ts +8 -1
  238. package/dist/memory/journal/session-journal.js +10 -0
  239. package/dist/memory/project/baselines.d.ts +1 -1
  240. package/dist/memory/project/dir/reticle-dir.d.ts +3 -1
  241. package/dist/memory/project/dir/reticle-dir.js +1 -0
  242. package/dist/memory/project/fs/fs-port.js +44 -5
  243. package/dist/memory/project/fs/write-atomic.d.ts +26 -0
  244. package/dist/memory/project/fs/write-atomic.js +48 -0
  245. package/dist/memory/project/project-tools.js +3 -1
  246. package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
  247. package/dist/memory/recall/prior/connection-memory.js +8 -0
  248. package/dist/memory/recall/registered-projects.d.ts +2 -1
  249. package/dist/portal/bridge/auth-failure-reason.d.ts +1 -1
  250. package/dist/portal/bridge/auth-failure-reason.js +9 -4
  251. package/dist/portal/bridge/bridge.d.ts +8 -0
  252. package/dist/portal/bridge/bridge.js +43 -2
  253. package/dist/portal/input/js-coverage.d.ts +25 -0
  254. package/dist/portal/input/js-coverage.js +30 -0
  255. package/dist/portal/input/real-input.d.ts +11 -0
  256. package/dist/portal/input/real-input.js +31 -5
  257. package/dist/portal/pool/browser-pool.d.ts +10 -0
  258. package/dist/portal/pool/browser-pool.js +10 -0
  259. package/dist/portal/pool/playwright-launcher.d.ts +6 -0
  260. package/dist/portal/pool/playwright-launcher.js +21 -13
  261. package/dist/portal/pool/pool-contract.d.ts +2 -0
  262. package/dist/portal/pool/storage-seed.d.ts +2 -1
  263. package/dist/portal/pool/zero-install.d.ts +27 -0
  264. package/dist/portal/pool/zero-install.js +54 -0
  265. package/dist/portal/realm/web-realm.js +6 -0
  266. package/dist/portal/session/dev-server/dev-command.d.ts +2 -2
  267. package/dist/portal/session/dev-server/dev-command.js +30 -12
  268. package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
  269. package/dist/portal/session/dev-server/dev-server-probe.js +17 -1
  270. package/dist/portal/session/dev-server/route-status-probe.d.ts +24 -0
  271. package/dist/portal/session/dev-server/route-status-probe.js +57 -0
  272. package/dist/portal/session/dev-server/served-document.d.ts +21 -0
  273. package/dist/portal/session/dev-server/served-document.js +57 -0
  274. package/dist/portal/session/facts/background-traffic.d.ts +9 -0
  275. package/dist/portal/session/facts/background-traffic.js +25 -0
  276. package/dist/portal/session/facts/observed-state.d.ts +11 -0
  277. package/dist/portal/session/facts/observed-state.js +34 -0
  278. package/dist/portal/session/live-control-tools.js +48 -3
  279. package/dist/portal/session/no-session-diagnosis.d.ts +29 -0
  280. package/dist/portal/session/no-session-diagnosis.js +62 -5
  281. package/dist/portal/session/no-session-next-action.d.ts +13 -0
  282. package/dist/portal/session/no-session-next-action.js +48 -9
  283. package/dist/portal/session/no-session-watch.d.ts +18 -0
  284. package/dist/portal/session/no-session-watch.js +88 -2
  285. package/dist/portal/session/presence/attachment-history.d.ts +13 -2
  286. package/dist/portal/session/session-health.d.ts +61 -0
  287. package/dist/portal/session/session-health.js +43 -3
  288. package/dist/portal/session/session-info.d.ts +6 -0
  289. package/dist/portal/session/session-manager.d.ts +29 -6
  290. package/dist/portal/session/session-manager.js +42 -5
  291. package/dist/portal/session/session-reaper.d.ts +28 -0
  292. package/dist/portal/session/session-reaper.js +49 -1
  293. package/dist/portal/session/session-successor.d.ts +11 -0
  294. package/dist/portal/session/session-successor.js +14 -1
  295. package/dist/portal/session/session-verdict-facts.d.ts +28 -0
  296. package/dist/portal/session/session-verdict-facts.js +15 -0
  297. package/dist/portal/session/session.d.ts +10 -0
  298. package/dist/portal/session/session.js +34 -26
  299. package/dist/surface/http-server.d.ts +2 -1
  300. package/dist/surface/http-server.js +4 -2
  301. package/dist/surface/mcp/mcp-proxy.d.ts +2 -2
  302. package/dist/surface/mcp/mcp-proxy.js +5 -5
  303. package/dist/surface/mcp/mcp.js +4 -2
  304. package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +13 -1
  305. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +27 -2
  306. package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
  307. package/dist/surface/mcp/server-instructions.js +21 -8
  308. package/dist/surface/tools/act/after-match.d.ts +17 -0
  309. package/dist/surface/tools/act/after-match.js +30 -0
  310. package/dist/surface/tools/act/already-true.d.ts +40 -0
  311. package/dist/surface/tools/act/already-true.js +36 -0
  312. package/dist/surface/tools/act/durable.d.ts +69 -0
  313. package/dist/surface/tools/act/durable.js +139 -0
  314. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +19 -6
  315. package/dist/surface/tools/act/navigation/navigate-arrival.js +60 -10
  316. package/dist/surface/tools/act/navigation/navigate-result.d.ts +1 -12
  317. package/dist/surface/tools/act/navigation/navigate-result.js +21 -3
  318. package/dist/surface/tools/act/proved-controls.d.ts +9 -0
  319. package/dist/surface/tools/act/proved-controls.js +11 -0
  320. package/dist/surface/tools/act/sequence-grade.d.ts +5 -0
  321. package/dist/surface/tools/act/sequence-grade.js +32 -1
  322. package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
  323. package/dist/surface/tools/act/settle-in-flight.js +29 -6
  324. package/dist/surface/tools/act-capsule.d.ts +7 -0
  325. package/dist/surface/tools/act-capsule.js +68 -8
  326. package/dist/surface/tools/act-sequence-tool.js +25 -8
  327. package/dist/surface/tools/act-tools.js +51 -74
  328. package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
  329. package/dist/surface/tools/args/numeric-bounds.js +30 -3
  330. package/dist/surface/tools/assert/assert-grade.js +26 -3
  331. package/dist/surface/tools/assert/assert-verdict.js +32 -6
  332. package/dist/surface/tools/browser-tools.js +17 -4
  333. package/dist/surface/tools/coverage-tools.js +27 -1
  334. package/dist/surface/tools/dynamic-tools.js +6 -0
  335. package/dist/surface/tools/error-recovery.js +4 -4
  336. package/dist/surface/tools/feedback-invite.js +6 -4
  337. package/dist/surface/tools/feedback-tools.d.ts +2 -1
  338. package/dist/surface/tools/feedback-tools.js +1 -1
  339. package/dist/surface/tools/harness-explore.d.ts +3 -2
  340. package/dist/surface/tools/harness-explore.js +7 -6
  341. package/dist/surface/tools/invoke-tool.js +17 -4
  342. package/dist/surface/tools/lease-hint.d.ts +8 -0
  343. package/dist/surface/tools/lease-hint.js +10 -0
  344. package/dist/surface/tools/lease-tools.d.ts +16 -28
  345. package/dist/surface/tools/lease-tools.js +68 -11
  346. package/dist/surface/tools/lineage-tools.js +1 -1
  347. package/dist/surface/tools/live-call-text.js +4 -1
  348. package/dist/surface/tools/observe-tools.js +23 -7
  349. package/dist/surface/tools/real-input-attempt.d.ts +0 -5
  350. package/dist/surface/tools/real-input-attempt.js +1 -1
  351. package/dist/surface/tools/tool-kit.d.ts +5 -0
  352. package/dist/surface/tools/tool-kit.js +6 -1
  353. package/dist/surface/tools/tool-surface.js +17 -5
  354. package/dist/surface/tools/tools.js +68 -29
  355. package/dist/telemetry/argument-shape.d.ts +0 -2
  356. package/dist/telemetry/argument-shape.js +0 -4
  357. package/dist/telemetry/error-class.d.ts +2 -3
  358. package/dist/telemetry/error-class.js +2 -3
  359. package/dist/telemetry/feedback-local.d.ts +1 -0
  360. package/dist/telemetry/feedback-local.js +1 -3
  361. package/dist/telemetry/feedback.d.ts +1 -1
  362. package/dist/telemetry/harness-drive.d.ts +0 -2
  363. package/dist/telemetry/harness-drive.js +0 -4
  364. package/dist/telemetry/hud-metrics.d.ts +32 -0
  365. package/dist/telemetry/hud-metrics.js +95 -0
  366. package/dist/telemetry/session-metrics.d.ts +5 -0
  367. package/dist/telemetry/session-metrics.js +15 -1
  368. package/dist/wire-journal.d.ts +34 -0
  369. package/dist/wire-journal.js +104 -0
  370. package/dist/workspace-packages.d.ts +0 -2
  371. package/dist/workspace-packages.js +1 -9
  372. package/docs/README.md +5 -2
  373. package/docs/actions.mdx +3 -3
  374. package/docs/agent-cheatsheet.md +4 -4
  375. package/docs/architecture.md +6 -6
  376. package/docs/best-practices.mdx +1 -1
  377. package/docs/capabilities.mdx +32 -1
  378. package/docs/cli/cloud.mdx +21 -9
  379. package/docs/cli/doctor.mdx +4 -0
  380. package/docs/cli/feedback.mdx +2 -2
  381. package/docs/cli/gate.mdx +9 -4
  382. package/docs/cli/init.mdx +1 -0
  383. package/docs/cli/report.mdx +44 -0
  384. package/docs/cli/update.mdx +10 -0
  385. package/docs/cli.mdx +9 -0
  386. package/docs/desktop.mdx +14 -10
  387. package/docs/environment.mdx +87 -0
  388. package/docs/faq.mdx +18 -16
  389. package/docs/first-drive.md +5 -13
  390. package/docs/flows.md +32 -2
  391. package/docs/frameworks.mdx +140 -9
  392. package/docs/getting-started.md +3 -3
  393. package/docs/http-transport.md +6 -3
  394. package/docs/human-control.md +2 -2
  395. package/docs/index.mdx +16 -6
  396. package/docs/install-agentic.mdx +37 -1
  397. package/docs/install-manual.mdx +1 -1
  398. package/docs/integration-patterns.md +3 -3
  399. package/docs/multi-agent-testing.md +1 -1
  400. package/docs/packages/babel-plugin.mdx +1 -1
  401. package/docs/packages/browser.mdx +2 -2
  402. package/docs/packages/react.mdx +2 -2
  403. package/docs/packages/tauri.mdx +1 -1
  404. package/docs/packages/vite-plugin.mdx +2 -2
  405. package/docs/packages.mdx +9 -9
  406. package/docs/predicates.mdx +176 -7
  407. package/docs/quickstart.mdx +2 -2
  408. package/docs/recipes.mdx +2 -2
  409. package/docs/state-management.mdx +2 -2
  410. package/docs/system-map.md +1 -1
  411. package/docs/telemetry.md +1 -1
  412. package/docs/token-efficiency.md +3 -3
  413. package/docs/tools/act-and-wait.mdx +1 -0
  414. package/docs/tools/act-sequence.mdx +1 -1
  415. package/docs/tools/console.mdx +1 -1
  416. package/docs/tools/inspect.mdx +1 -1
  417. package/docs/tools/network.mdx +1 -1
  418. package/docs/tools/query.mdx +11 -11
  419. package/docs/tools/sessions.mdx +1 -1
  420. package/docs/tools/snapshot.mdx +1 -1
  421. package/docs/tools/state.mdx +1 -1
  422. package/docs/tools/wait-for.mdx +1 -1
  423. package/docs/usage.md +8 -7
  424. package/docs/use-cases.mdx +157 -0
  425. package/docs/vs-screenshots.mdx +3 -3
  426. package/docs/what-is-recorded.md +62 -0
  427. package/docs/what-is-reticle.mdx +17 -20
  428. package/docs/why-reticle.mdx +14 -21
  429. package/package.json +11 -10
  430. package/dist/judgement/capsule/minimize.d.ts +0 -12
  431. package/dist/judgement/capsule/minimize.js +0 -22
  432. package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
  433. package/dist/judgement/outcome/predicate-to-expect.js +0 -148
  434. package/dist/judgement/phenomena/phenomena.d.ts +0 -26
  435. package/dist/judgement/phenomena/phenomena.js +0 -101
  436. package/dist/language/flows/flow-report.d.ts +0 -22
  437. package/dist/language/flows/flow-report.js +0 -106
@@ -0,0 +1,90 @@
1
+ import { ReticleDir } from '@reticlehq/core';
2
+ /**
3
+ * May retention delete this to reclaim disk?
4
+ *
5
+ * MEMORY is why `.reticle/` exists — what a flow replays from, what a deviation is measured
6
+ * against. It is small, it is never evicted, and it must keep being written however full the disk
7
+ * is. EVIDENCE is what one drive left behind: journals, run artifacts, pixel diffs. It is large, it
8
+ * ages out, and losing the oldest of it costs nothing anybody can name.
9
+ */
10
+ export const Durability = {
11
+ MEMORY: 'memory',
12
+ EVIDENCE: 'evidence',
13
+ };
14
+ /**
15
+ * Does this belong in the repository, or only on the machine that wrote it?
16
+ *
17
+ * COMMIT is the shareable half: a flow nobody else can replay is not a regression check. LOCAL is
18
+ * per-machine churn, and journals carry URLs, request and response bodies and page text from the
19
+ * app under test, so keeping them out of a shared repository is worth doing on its own account.
20
+ */
21
+ export const Sharing = {
22
+ COMMIT: 'commit',
23
+ LOCAL: 'local',
24
+ };
25
+ const localDir = { durability: Durability.EVIDENCE, sharing: Sharing.LOCAL, isDirectory: true };
26
+ const localFile = { durability: Durability.MEMORY, sharing: Sharing.LOCAL, isDirectory: false };
27
+ const sharedDir = { durability: Durability.MEMORY, sharing: Sharing.COMMIT, isDirectory: true };
28
+ const sharedFile = { durability: Durability.MEMORY, sharing: Sharing.COMMIT, isDirectory: false };
29
+ /**
30
+ * Every top-level name `.reticle/` can hold, classified on both axes.
31
+ *
32
+ * One list used to answer both questions with one answer, and they are not the same question.
33
+ * `capsules/` is committed — a fail-to-pass capsule a teammate cannot replay is not a regression
34
+ * check — AND it is evidence a drive left behind, which grows without bound. That cell did not
35
+ * exist, so the byte budget, which read the gitignore's list, structurally could not see capsules
36
+ * however large they got. Splitting the axes is what makes that sayable; it does not by itself say
37
+ * it, and `capsules/` stays MEMORY here until the budget is measured against it.
38
+ *
39
+ * Declaration order is the order the ignore file lists them, so the file a user already has on disk
40
+ * and the one written today are the same file.
41
+ */
42
+ export const WORKSPACE_TIERS = {
43
+ [ReticleDir.SESSIONS_SUBDIR]: localDir,
44
+ [ReticleDir.RUNS_SUBDIR]: localDir,
45
+ [ReticleDir.VISUAL_SUBDIR]: localDir,
46
+ // Write-only: a local copy of what was already sent. The outbox is the record.
47
+ [ReticleDir.FEEDBACK_SUBDIR]: localDir,
48
+ [ReticleDir.PROJECT_FILE]: localFile,
49
+ [ReticleDir.AMBIENT_FILE]: localFile,
50
+ [ReticleDir.ENVELOPES_FILE]: localFile,
51
+ [ReticleDir.FLAKE_FILE]: localFile,
52
+ // What THIS machine's drives covered, and the best each level reached — the gate's ratchet.
53
+ [ReticleDir.COVERAGE_FILE]: localFile,
54
+ [ReticleDir.TIERS_FILE]: localFile,
55
+ // The user's own record of what Reticle did for them, on THIS machine.
56
+ [ReticleDir.IMPACT_FILE]: localFile,
57
+ // This machine's conversation with the server. Its own doc comment says why committing it is
58
+ // harmful: one machine's pull cursor makes every other machine skip what it has not seen.
59
+ [ReticleDir.CLOUD_STATE_FILE]: localFile,
60
+ // Triage pulled back from the dashboard — a cache of somebody's decisions, re-pullable at will.
61
+ [ReticleDir.ISSUES_FILE]: localFile,
62
+ [ReticleDir.CONTRACT_FILE]: sharedFile,
63
+ [ReticleDir.FLOWS_SUBDIR]: sharedDir,
64
+ [ReticleDir.BASELINES_SUBDIR]: sharedDir,
65
+ [ReticleDir.CAPSULES_SUBDIR]: sharedDir,
66
+ [ReticleDir.INTENT_FILE]: sharedFile,
67
+ // The sharded form of the same ledger, and shared for the same reason.
68
+ [ReticleDir.INTENT_SUBDIR]: sharedDir,
69
+ [ReticleDir.CLOUD_LINK_FILE]: sharedFile,
70
+ // A hook is a decision the whole team shares, exactly like a package script: one that exists only
71
+ // on the machine that wrote it is a rule nobody else is following. It names COMMANDS and never a
72
+ // credential, so it is safe to commit — the same reasoning that puts `cloud.json` here while its
73
+ // API key stays in ~/.reticle.
74
+ [ReticleDir.HOOKS_FILE]: sharedFile,
75
+ // What a drive types into each labelled field. Committed for two reasons: a replay must send
76
+ // exactly what the recording sent or it is not a replay, and a value a model wrote once should be
77
+ // reviewable and editable by the team rather than regenerated differently on every machine.
78
+ [ReticleDir.FILL_VALUES_FILE]: sharedFile,
79
+ };
80
+ function namesWhere(match) {
81
+ return Object.entries(WORKSPACE_TIERS)
82
+ .filter(([, tier]) => match(tier))
83
+ .map(([name]) => name);
84
+ }
85
+ /** Machine-local directories, without the trailing slash the ignore file wants. */
86
+ export const LOCAL_DIRS = namesWhere((tier) => Sharing.LOCAL === tier.sharing && tier.isDirectory);
87
+ /** Machine-local files. */
88
+ export const LOCAL_FILES = namesWhere((tier) => Sharing.LOCAL === tier.sharing && !tier.isDirectory);
89
+ /** The directories retention sweeps, ignored or not. The byte budget and the count bounds read this. */
90
+ export const EVIDENCE_DIRS = namesWhere((tier) => Durability.EVIDENCE === tier.durability && tier.isDirectory);
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The key an envelope's statistics accumulate under.
3
+ *
4
+ * The envelope map was keyed on the raw `pathname`. On any app whose URLs carry ids — most of them —
5
+ * `/orders/1001` and `/orders/1002` are different keys, so every visit mints a fresh envelope
6
+ * holding one sample.
7
+ *
8
+ * The cost is not the file size. `deviation-report` skips any envelope below
9
+ * `MIN_ENVELOPE_SAMPLES`, so on such an app no envelope ever qualifies, every report answers
10
+ * "envelope too new", and the deviation feature never turns on at all — silently, permanently, with
11
+ * nothing anywhere saying why. The unbounded file and the dead feature are one defect.
12
+ *
13
+ * CONSERVATIVE on purpose. Over-collapsing is the worse error: merging two genuinely different
14
+ * routes builds a baseline from two different pages, and a confident comparison against the wrong
15
+ * population is more expensive than no comparison. So a segment is an id only when it could not
16
+ * plausibly be a route NAME.
17
+ *
18
+ * This is the statistics key alone. The observed path is untouched, because a report has to be able
19
+ * to say "/orders/1001 was slow" — the template tells you where to look, the path tells you what to
20
+ * open.
21
+ */
22
+ export declare function routeTemplate(pathname: string): string;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The key an envelope's statistics accumulate under.
3
+ *
4
+ * The envelope map was keyed on the raw `pathname`. On any app whose URLs carry ids — most of them —
5
+ * `/orders/1001` and `/orders/1002` are different keys, so every visit mints a fresh envelope
6
+ * holding one sample.
7
+ *
8
+ * The cost is not the file size. `deviation-report` skips any envelope below
9
+ * `MIN_ENVELOPE_SAMPLES`, so on such an app no envelope ever qualifies, every report answers
10
+ * "envelope too new", and the deviation feature never turns on at all — silently, permanently, with
11
+ * nothing anywhere saying why. The unbounded file and the dead feature are one defect.
12
+ *
13
+ * CONSERVATIVE on purpose. Over-collapsing is the worse error: merging two genuinely different
14
+ * routes builds a baseline from two different pages, and a confident comparison against the wrong
15
+ * population is more expensive than no comparison. So a segment is an id only when it could not
16
+ * plausibly be a route NAME.
17
+ *
18
+ * This is the statistics key alone. The observed path is untouched, because a report has to be able
19
+ * to say "/orders/1001 was slow" — the template tells you where to look, the path tells you what to
20
+ * open.
21
+ */
22
+ /** Stands in for any id segment. Not a valid path segment itself, so it cannot collide with one. */
23
+ const ID = ':id';
24
+ /** 8+ hex characters: a token, a hash, a short uuid. Below that it is as likely to be a word. */
25
+ const LONG_HEX = /^[0-9a-f]{8,}$/i;
26
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
27
+ const ALL_DIGITS = /^\d+$/;
28
+ /**
29
+ * A year collapses too, and that is a deliberate trade rather than an oversight: `/reports/2024`
30
+ * and `/reports/2025` are the same page with different data, which is exactly what one envelope
31
+ * should describe. Distinguishing a year from an id needs to know the app, and guessing would
32
+ * reintroduce the split this exists to remove.
33
+ */
34
+ function isIdSegment(segment) {
35
+ return ALL_DIGITS.test(segment) || UUID.test(segment) || LONG_HEX.test(segment);
36
+ }
37
+ export function routeTemplate(pathname) {
38
+ if (0 === pathname.length)
39
+ return pathname;
40
+ return pathname
41
+ .split('/')
42
+ .map((segment) => (isIdSegment(segment) ? ID : segment))
43
+ .join('/');
44
+ }
@@ -3,6 +3,7 @@ import { type OnboardingStep } from '@reticlehq/core/telemetry';
3
3
  import type { AmbientCounts } from '@reticlehq/engine/window/ambient.js';
4
4
  import { type ProjectId, type JournalAction } from '@reticlehq/core';
5
5
  import type { FileSystemPort } from '../project/fs/fs-port.js';
6
+ import { type PruneWorkspaceOptions } from './on-disk/startup-maintenance.js';
6
7
  /**
7
8
  * Session teardown: the durable half of ending a session. Two things must happen when a tab disconnects,
8
9
  * and neither did before — the journal batches events and only writes at its flush threshold, so the tail
@@ -44,8 +45,10 @@ export interface SessionEndTarget {
44
45
  interface SessionEndDeps {
45
46
  fs: FileSystemPort;
46
47
  reticleRoot: string;
47
- /** Journaling/persistence off (opt-out) → teardown is a no-op. */
48
+ /** Journaling/persistence off (opt-out) → teardown is a no-op. Retention still runs. */
48
49
  enabled: boolean;
50
+ /** What this project is willing to keep — the `retain` block of its `.reticle.json`. */
51
+ retain?: PruneWorkspaceOptions;
49
52
  /**
50
53
  * The one clock in this file, injected — the run artifact stamps `createdAt` from it.
51
54
  *
@@ -76,8 +79,16 @@ interface SessionEndDeps {
76
79
  * this handler follows.
77
80
  */
78
81
  flows?: {
79
- save: (program: DriveProgram, annotations?: undefined, projectId?: ProjectId) => Promise<unknown>;
82
+ saveJourney: (program: DriveProgram, projectId: ProjectId | undefined, prefix: string) => Promise<unknown>;
80
83
  };
84
+ /**
85
+ * The flow store for a session's own `.reticle`. Drive flows used to go to `flows` above — the
86
+ * daemon's launch directory — while replay reads the session's project, so a daemon started
87
+ * anywhere else saved flows nothing could replay. Absent, `flows` is used.
88
+ */
89
+ flowsAt?: (root: string) => NonNullable<SessionEndDeps['flows']>;
90
+ /** Who is driving — stamped on each saved flow. Injected: teardown does not reach into MCP. */
91
+ author?: () => DriveProgram['author'];
81
92
  /**
82
93
  * Called when a run artifact is written, so cloud sync can cycle instead of waiting for its timer.
83
94
  *
@@ -1,8 +1,17 @@
1
- import { driveFlowsFrom } from './drive-flow.js';
1
+ import { DRIVE_FLOW_PREFIX, driveFlowsFrom, } from './drive-flow.js';
2
+ import { log } from '../../log.js';
3
+ /** What a session's teardown writes to the daemon log when a by-product fails to save. */
4
+ const SESSION_END_LOG = {
5
+ FLOW_SAVE_FAILED: 'reticle_drive_flow_save_failed',
6
+ RUN_RECORD_FAILED: 'reticle_drive_run_record_failed',
7
+ };
8
+ const errorMessage = (error) => error instanceof Error ? error.message : String(error);
9
+ import { journalActionsPath, sessionDirPath } from '../project/dir/reticle-dir.js';
10
+ import { asSessionId } from '@reticlehq/core';
2
11
  import { OnboardingPhase, OnboardingStepStatus, } from '@reticlehq/core/telemetry';
3
12
  import { AmbientStore } from './ambient-store.js';
4
13
  import { subjectOf } from '@reticlehq/core';
5
- import { pruneSessions } from './on-disk/retention.js';
14
+ import { pruneWorkspace } from './on-disk/startup-maintenance.js';
6
15
  import { buildVerificationRun } from '../../judgement/runs/artifact/build-verification-run.js';
7
16
  import { driveRunFrom, driveRunId } from '../../judgement/runs/drive-run.js';
8
17
  import { RunStore } from '../../judgement/runs/artifact/run-store.js';
@@ -13,8 +22,28 @@ import { RunStore } from '../../judgement/runs/artifact/run-store.js';
13
22
  */
14
23
  export function makeSessionEnd(deps) {
15
24
  return async (session) => {
16
- if (!deps.enabled)
25
+ /*
26
+ * Retention is maintenance of a directory, not a part of journalling, so it runs either way.
27
+ *
28
+ * Teardown used to return here, and this is the one setting under which that mattered most:
29
+ * somebody switches journalling off BECAUSE `.reticle/` got too big, and switching it off was
30
+ * what stopped anything ever deleting what was already there. It also stopped the sweep of
31
+ * visual diffs, feedback copies and run artifacts, none of which need the journal to be written
32
+ * at all. The daemon's own start-path sweep was gated on the same flag, so there was no second
33
+ * site still running — both are ungated now.
34
+ */
35
+ const sweep = async () => {
36
+ try {
37
+ await pruneWorkspace(deps.fs, session.artifactRoot ?? deps.reticleRoot, deps.liveSessionIds?.() ?? new Set(), deps.retain ?? {});
38
+ }
39
+ catch {
40
+ // retention is best-effort maintenance; never surface at teardown
41
+ }
42
+ };
43
+ if (!deps.enabled) {
44
+ await sweep();
17
45
  return;
46
+ }
18
47
  try {
19
48
  await session.flushJournal();
20
49
  }
@@ -56,24 +85,27 @@ export function makeSessionEnd(deps) {
56
85
  // would buy under a minute of latency for a mutable handle held across two wiring sites.
57
86
  // A DRIVE IS A REGRESSION TEST, and it used to need somebody to remember to say so.
58
87
  //
59
- // `record{start}` + `flow_save` is the agent-facing route and agents do not take it: the corpus
60
- // measures 3 of 33 flows mutation-testable and 6 of 112 steps declaring a consequence, while the
61
- // engine catches 84 of the 86 bugs it structurally can. The ceiling is how many flows EXIST that
62
- // could go red, so a drive that is never saved is a regression test paid for and thrown away.
88
+ // `record{start}` + `flow_save` is the agent-facing route and agents do not take it: few saved
89
+ // flows are mutation-testable and few steps declare a consequence, while the engine catches
90
+ // nearly every bug it structurally can. The ceiling is how many flows EXIST that could go red,
91
+ // so a drive that is never saved is a regression test paid for and thrown away.
63
92
  //
64
93
  // Saved only when the tape declared a consequence — see drive-flow.ts for why the alternative is
65
94
  // a false-green factory running once per session.
66
95
  try {
67
96
  await saveDrivenFlow(deps, session);
68
97
  }
69
- catch {
70
- // a flow is a by-product of the session; failing to write one never fails the teardown
98
+ catch (error) {
99
+ // A flow is a by-product of the session; failing to write one never fails the teardown. It is
100
+ // logged, because a drive that leaves no flow and no reason is otherwise undiagnosable.
101
+ log(SESSION_END_LOG.FLOW_SAVE_FAILED, { error: errorMessage(error) });
71
102
  }
72
103
  try {
73
104
  await recordDriveRun(deps, session);
74
105
  }
75
- catch {
76
- // an artifact is a report ABOUT the session; failing to write one never fails the teardown
106
+ catch (error) {
107
+ // An artifact is a report ABOUT the session; failing to write one never fails the teardown.
108
+ log(SESSION_END_LOG.RUN_RECORD_FAILED, { error: errorMessage(error) });
77
109
  }
78
110
  try {
79
111
  // Bound the journal on disk HERE, not only at daemon start.
@@ -101,13 +133,31 @@ export function makeSessionEnd(deps) {
101
133
  // The root this session actually journalled into. Pruning the daemon's tree instead meant a
102
134
  // per-project workspace was never swept at all, so the one place journals really accumulate
103
135
  // was the one place retention never ran.
104
- await pruneSessions(deps.fs, session.artifactRoot ?? deps.reticleRoot, {
105
- live: deps.liveSessionIds?.(),
106
- });
136
+ /*
137
+ * A session nobody ever drove keeps no journal.
138
+ *
139
+ * The observers run from the moment the SDK attaches, so an idle tab on a busy page
140
+ * accumulates megabytes without a single tool call. The cost is not the disk: those
141
+ * directories occupy retention SLOTS, so a journal that could answer a verdict question is
142
+ * evicted by one that was never asked one.
143
+ *
144
+ * Decided at the END and never by gating CAPTURE. Whether a tool call will happen is not
145
+ * knowable while the events that would answer it are being recorded, and gating capture on it
146
+ * would leave the first assertion of a session with nothing to read.
147
+ */
148
+ await dropUndrivenJournal(deps.fs, session.artifactRoot ?? deps.reticleRoot, asSessionId(session.id));
107
149
  }
108
150
  catch {
109
- // retention is best-effort maintenance; never surface at teardown
151
+ // teardown must never throw: the tab is already gone
110
152
  }
153
+ // EVERY tier, not just sessions. Visual diffs and feedback copies were pruned only at daemon
154
+ // start, against the DAEMON's root — which for a globally registered daemon is `$HOME` and not
155
+ // the project at all, so the two tiers that only ever grow in a project workspace were the two
156
+ // never swept there. Same defect as the one this line already fixed for sessions, one move
157
+ // behind. The byte budget rides along for the same reason: it was wired at daemon start too.
158
+ //
159
+ // Outside the try above, so a failure anywhere in teardown still leaves the directory swept.
160
+ await sweep();
111
161
  };
112
162
  }
113
163
  /**
@@ -156,13 +206,15 @@ export async function recordDriveRun(deps, session) {
156
206
  /** Persist what this session drove as a replayable flow, when it declared anything provable. */
157
207
  async function saveDrivenFlow(deps, session) {
158
208
  const takeTape = deps.takeAmbientTape;
159
- const flows = deps.flows;
209
+ const flows = deps.flowsAt?.(session.artifactRoot ?? deps.reticleRoot) ?? deps.flows;
160
210
  if (takeTape === undefined || flows === undefined)
161
211
  return;
162
212
  // One flow per journey the session contained, not one flow per session — see drive-flow.ts.
163
- const { programs, outcome } = driveFlowsFrom(session.id, takeTape());
213
+ const { programs, outcome } = driveFlowsFrom(takeTape());
214
+ const author = deps.author?.();
164
215
  for (const program of programs) {
165
- await flows.save(program, undefined, session.projectId);
216
+ // Folded into any saved copy of the same journey, so each session does not add a near-copy.
217
+ await flows.saveJourney(author === undefined ? program : { ...program, author }, session.projectId, DRIVE_FLOW_PREFIX);
166
218
  }
167
219
  // The funnel step that makes the SECOND run cheap.
168
220
  //
@@ -177,3 +229,21 @@ async function saveDrivenFlow(deps, session) {
177
229
  ...(outcome.unprovenSteps === undefined ? {} : { reason: 'no_declared_consequence' }),
178
230
  });
179
231
  }
232
+ /**
233
+ * Remove this session's journal when it served no tool call.
234
+ *
235
+ * "Served no tool call" is read off the actions ledger, which is written one line per call: absent
236
+ * or empty means nothing was ever driven here. Best-effort like every other maintenance step —
237
+ * a session that cannot be tidied is not a session that failed.
238
+ */
239
+ async function dropUndrivenJournal(fs, root, sessionId) {
240
+ try {
241
+ const actions = await fs.readFile(journalActionsPath(root, sessionId)).catch(() => '');
242
+ if (actions.trim().length > 0)
243
+ return;
244
+ await fs.rm(sessionDirPath(root, sessionId));
245
+ }
246
+ catch {
247
+ // tidying is never the reason a teardown fails
248
+ }
249
+ }
@@ -1,6 +1,6 @@
1
1
  import { type JournalAction, type JournalWriteLoss, type ReticleEvent } from '@reticlehq/core';
2
2
  import type { FileSystemPort } from '../project/fs/fs-port.js';
3
- import type { JournalReadLoss } from './journal-recorder.js';
3
+ import type { JournalLedgerSize, JournalReadLoss } from './journal-recorder.js';
4
4
  /**
5
5
  * The two ceilings a durable read is held to.
6
6
  *
@@ -115,6 +115,13 @@ export declare class SessionJournal {
115
115
  * file, as opposed to what a bounded read declined to hand back.
116
116
  */
117
117
  readWriteLoss(): Promise<JournalWriteLoss | undefined>;
118
+ /**
119
+ * The ledger's size against its ceiling, once the size is known.
120
+ *
121
+ * `undefined` before the first append, because `#eventBytes` is seeded from `stat` at that point
122
+ * and a zero reported before it is measured would be a claim rather than a reading.
123
+ */
124
+ ledger(): JournalLedgerSize | undefined;
118
125
  appendAction(action: JournalAction): Promise<void>;
119
126
  readEvents(): Promise<ReticleEvent[]>;
120
127
  /**
@@ -281,6 +281,16 @@ export class SessionJournal {
281
281
  async readWriteLoss() {
282
282
  return this.#closureReport();
283
283
  }
284
+ /**
285
+ * The ledger's size against its ceiling, once the size is known.
286
+ *
287
+ * `undefined` before the first append, because `#eventBytes` is seeded from `stat` at that point
288
+ * and a zero reported before it is measured would be a claim rather than a reading.
289
+ */
290
+ ledger() {
291
+ const bytes = this.#eventBytes;
292
+ return bytes === undefined ? undefined : { bytes, capBytes: this.#eventBytesCap };
293
+ }
284
294
  /**
285
295
  * Close the ledger for good, and declare the loss twice — durably, then in-band.
286
296
  *
@@ -1,4 +1,4 @@
1
- export interface Baseline {
1
+ interface Baseline {
2
2
  name: string;
3
3
  lines: string[];
4
4
  route: string;
@@ -15,7 +15,7 @@ export interface ReticleDirPaths {
15
15
  project: string;
16
16
  /**.../.reticle/impact.json (what Reticle has done for this user, local only) */
17
17
  impact: string;
18
- /**.../.reticle/intent.json (what changes were supposed to make true — git-checked) */
18
+ /**.../.reticle/intent.json — the LEGACY single ledger, read until the first write migrates it into .reticle/intent/ */
19
19
  intent: string;
20
20
  /**.../.reticle/fill-values.json (what a drive types into each labelled field — git-checked) */
21
21
  fillValues: string;
@@ -33,6 +33,8 @@ export interface ReticleDirPaths {
33
33
  capsules: string;
34
34
  /**.../.reticle/flake.json (per-flow flake ledger) */
35
35
  flake: string;
36
+ /**.../.reticle/coverage.json (app-wide coverage ledger, all six levels) */
37
+ coverage: string;
36
38
  /**.../.reticle/assertion-tiers.json (last-passing assertion tiers; anti-reward-hacking baseline) */
37
39
  tiers: string;
38
40
  }
@@ -18,6 +18,7 @@ export function reticleDirPaths(root) {
18
18
  ambient: join(root, ReticleDir.AMBIENT_FILE),
19
19
  capsules: join(root, ReticleDir.CAPSULES_SUBDIR),
20
20
  flake: join(root, ReticleDir.FLAKE_FILE),
21
+ coverage: join(root, ReticleDir.COVERAGE_FILE),
21
22
  tiers: join(root, ReticleDir.TIERS_FILE),
22
23
  };
23
24
  }
@@ -1,11 +1,46 @@
1
1
  import { access, appendFile, mkdir, open, readdir, readFile, realpath, rename, rm, stat, writeFile, } from 'node:fs/promises';
2
2
  /** The JSONL record terminator, as a byte — the only boundary a capped tail read can align to. */
3
3
  const NEWLINE_BYTE = 0x0a;
4
+ /**
5
+ * The largest single `read(2)` this port will ask for: 2^31 - 1, the int32 ceiling.
6
+ *
7
+ * Past it `fs.read` does not throw — it ABORTS the process on a V8 assertion
8
+ * (`node::fs::Read ... Assertion failed: args[3]->IsInt32()`, where args[3] is the length). That is
9
+ * not catchable, so one oversized journal takes the daemon down and with it every session on the
10
+ * machine, including unrelated ones. Reported from the field exactly that way.
11
+ *
12
+ * Reachable in practice because `Buffer.allocUnsafe` happily allocates well past this, so a
13
+ * `size - start` over 2 GiB allocates fine and then kills node at the syscall. A `.reticle/sessions`
14
+ * has been observed in the multi-gigabyte range, so "no journal is ever that big" is not a bound
15
+ * anybody is enforcing.
16
+ *
17
+ * Clamping rather than throwing is deliberate: a caller reading a tail wants the newest records, and
18
+ * returning the last 2 GiB of them is a better answer than an error. The boundary realignment below
19
+ * already handles a window that opens mid-record, which is exactly what a clamp produces.
20
+ */
21
+ const MAX_SINGLE_READ_BYTES = 2_147_483_647;
22
+ /**
23
+ * Refuse an offset that cannot survive the arithmetic below.
24
+ *
25
+ * `length = size - start`, so a non-integer or negative offset produces a non-integer or negative
26
+ * length, and those fail in two different ways one layer apart: `Buffer.allocUnsafe` throws a
27
+ * RangeError for a fractional length, while an oversized one reaches `fs.read` and aborts the
28
+ * process. Checking here means the caller gets one predictable rejection instead of either.
29
+ */
30
+ function requireByteCount(label, value) {
31
+ if (value === undefined)
32
+ return;
33
+ if (!Number.isSafeInteger(value) || value < 0) {
34
+ throw new RangeError(`${label} must be a non-negative safe integer, got ${String(value)} — a fractional or negative byte count cannot reach a read syscall safely`);
35
+ }
36
+ }
4
37
  /** Production adapter — the single import site of node:fs/promises in the server. */
5
38
  export function createNodeFileSystem() {
6
39
  return {
7
40
  readFile: (path) => readFile(path, 'utf8'),
8
41
  readFileFrom: async (path, byteOffset, maxBytes) => {
42
+ requireByteCount('byteOffset', byteOffset);
43
+ requireByteCount('maxBytes', maxBytes);
9
44
  const fh = await open(path, 'r');
10
45
  try {
11
46
  const { size } = await fh.stat();
@@ -18,14 +53,18 @@ export function createNodeFileSystem() {
18
53
  // One byte BEFORE that cut, so a cut landing exactly on a record boundary is recognised as
19
54
  // one instead of sacrificing the record after it.
20
55
  const start = capped ? cut - 1 : byteOffset;
21
- const length = size - start;
56
+ // Clamped to the int32 ceiling: past it `fs.read` aborts the process rather than throwing.
57
+ // Keep the NEWEST bytes, matching what every caller of a tail read is asking for.
58
+ const want = size - start;
59
+ const length = Math.min(want, MAX_SINGLE_READ_BYTES);
60
+ const readFrom = start + (want - length);
22
61
  const buf = Buffer.allocUnsafe(length);
23
- const { bytesRead } = await fh.read(buf, 0, length, start);
62
+ const { bytesRead } = await fh.read(buf, 0, length, readFrom);
24
63
  // ONLY what the read returned. `allocUnsafe` hands back whatever was last in that heap
25
64
  // block, and `fstat` runs one syscall ahead of `read` — so a file truncated in between, or
26
65
  // any short read, would otherwise decode that memory and hand it back as journal text.
27
66
  const got = buf.subarray(0, bytesRead);
28
- if (!capped)
67
+ if (!capped && readFrom === start)
29
68
  return { text: got.toString('utf8'), size, from: start };
30
69
  // The ceiling moved the start, so the window may open inside a record whose head was never
31
70
  // read. Drop up to the first boundary in BYTES, before decoding: `\n` is one byte and
@@ -35,11 +74,11 @@ export function createNodeFileSystem() {
35
74
  // No boundary in the whole window: there is no record here that can be read, and half of
36
75
  // one is worse than none. `from` still says how far the read reached.
37
76
  if (-1 === newline)
38
- return { text: '', size, from: start + bytesRead };
77
+ return { text: '', size, from: readFrom + bytesRead };
39
78
  return {
40
79
  text: got.subarray(newline + 1).toString('utf8'),
41
80
  size,
42
- from: start + newline + 1,
81
+ from: readFrom + newline + 1,
43
82
  };
44
83
  }
45
84
  finally {
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Write a file so a reader never sees a half-written one, and two writers never see each other's.
3
+ *
4
+ * Write to a temporary sibling, then rename. The rename is what makes it atomic: on a POSIX
5
+ * filesystem it either replaces the destination entirely or does not happen, so a reader gets the
6
+ * old file or the new one and never a truncated middle.
7
+ *
8
+ * TWO properties, and the second is the one that was missing. Five stores in this package built
9
+ * this by hand and named the temp file after the DESTINATION — `${path}.tmp` — which is shared by
10
+ * every process writing that path. Two daemons on one repository then write the same temp file:
11
+ * one truncates the other mid-write and renames the remains into place, and the survivor is a file
12
+ * neither of them produced. Two other stores already pid-suffixed theirs, so the fix was known and
13
+ * had simply never been carried to the rest.
14
+ *
15
+ * Running parallel agent sessions against one checkout is the ordinary way to reach this, not an
16
+ * exotic one.
17
+ *
18
+ * The temp file stays BESIDE its destination rather than in the system temp directory, because a
19
+ * rename across devices is not atomic and `/tmp` is routinely a different device.
20
+ *
21
+ * One helper rather than five corrected copies: the sixth store gets this for free and cannot get
22
+ * it wrong, which is the only version of this fix that stays fixed.
23
+ */
24
+ import type { FileSystemPort } from './fs-port.js';
25
+ export declare function tempNameFor(path: string, pid?: number, seq?: number): string;
26
+ export declare function writeFileAtomic(fs: FileSystemPort, path: string, contents: string): Promise<void>;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Unique per WRITE, not per process.
3
+ *
4
+ * The pid separates two daemons. It does not separate two concurrent writes inside ONE daemon —
5
+ * `Promise.all` over two store saves, or two scheduled flushes of the same ledger — and those share
6
+ * a pid, so a pid-only name puts them straight back on the same temp file. The first version of
7
+ * this helper had exactly that hole and its own concurrency test found it.
8
+ *
9
+ * A counter rather than `Math.random()`: rule 7 keeps the clock and the random source out of pure
10
+ * logic, and a monotonic counter is deterministic, collision-free within a process, and needs
11
+ * neither.
12
+ */
13
+ let writeSeq = 0;
14
+ export function tempNameFor(path, pid = process.pid, seq) {
15
+ writeSeq += 1;
16
+ return `${path}.${String(pid)}.${String(seq ?? writeSeq)}.tmp`;
17
+ }
18
+ /**
19
+ * What Windows answers, for a moment, when another writer is still replacing the destination.
20
+ * POSIX never refuses such a rename; Windows does, and the save has to wait it out rather than
21
+ * fail. Seen on Windows CI with two writers on one path.
22
+ */
23
+ const TRANSIENT_RENAME = new Set(['EPERM', 'EBUSY', 'EACCES']);
24
+ const RENAME_ATTEMPTS = 6;
25
+ const RENAME_BACKOFF_MS = 15;
26
+ function isTransientRename(error) {
27
+ const code = error?.code;
28
+ return 'string' === typeof code && TRANSIENT_RENAME.has(code);
29
+ }
30
+ export async function writeFileAtomic(fs, path, contents) {
31
+ const tmp = tempNameFor(path);
32
+ await fs.writeFile(tmp, contents);
33
+ for (let attempt = 1;; attempt += 1) {
34
+ try {
35
+ await fs.rename(tmp, path);
36
+ return;
37
+ }
38
+ catch (error) {
39
+ if (attempt < RENAME_ATTEMPTS && isTransientRename(error)) {
40
+ await new Promise((resolve) => setTimeout(resolve, RENAME_BACKOFF_MS * attempt));
41
+ continue;
42
+ }
43
+ // The write failed; its temp file must not outlive it.
44
+ await fs.rm(tmp).catch(() => undefined);
45
+ throw error;
46
+ }
47
+ }
48
+ }
@@ -38,7 +38,9 @@ function numericDelta(before, after) {
38
38
  * "what's broken vs before" from the cloud. Absent creds / unreachable → undefined (agent stays local).
39
39
  */
40
40
  async function cloudRegression(deps, sessionId) {
41
- const config = resolveCloudConfig(process.env);
41
+ // The same resolver every caller uses (stored key for a linked repo, else the exported one);
42
+ // the environment alone only for an embedder that wires no linked-credential port.
43
+ const config = await (deps.linkedCloud?.() ?? Promise.resolve(resolveCloudConfig(process.env)));
42
44
  if (null === config)
43
45
  return undefined;
44
46
  let projectId;
@@ -63,3 +63,9 @@ export declare function hasAnyAppConnectedBefore(stateDir: string, port: number,
63
63
  * diagnosis, which is why it now shares this function instead of keeping a second copy of the rule.
64
64
  */
65
65
  export declare function hasProjectConnectedBefore(stateDir: string, port: number, projectId: string | undefined): boolean;
66
+ /**
67
+ * Has any of these projects connected before: the first-move question for a directory that speaks
68
+ * for more than one app (a monorepo root), or for none by name (an app wired by the plugin alone).
69
+ * The ids come from `projectIdsAt`; each is still asked the narrow question.
70
+ */
71
+ export declare function hasAnyProjectConnectedBefore(stateDir: string, port: number, projectIds: readonly string[]): boolean;
@@ -112,3 +112,11 @@ export function hasProjectConnectedBefore(stateDir, port, projectId) {
112
112
  return false;
113
113
  return hasAnyAppConnectedBefore(stateDir, port, projectId);
114
114
  }
115
+ /**
116
+ * Has any of these projects connected before: the first-move question for a directory that speaks
117
+ * for more than one app (a monorepo root), or for none by name (an app wired by the plugin alone).
118
+ * The ids come from `projectIdsAt`; each is still asked the narrow question.
119
+ */
120
+ export function hasAnyProjectConnectedBefore(stateDir, port, projectIds) {
121
+ return projectIds.some((id) => hasProjectConnectedBefore(stateDir, port, id));
122
+ }
@@ -1,5 +1,5 @@
1
1
  /** One project this machine has paired before, in the shape the no-session facts already carry. */
2
- export interface RegisteredProject {
2
+ interface RegisteredProject {
3
3
  directory: string;
4
4
  projectId?: string;
5
5
  }
@@ -52,3 +52,4 @@ export declare function isUnscopedRoot(directory: string, home: string): boolean
52
52
  * reporting it as such would tell a correctly-scoped daemon to go and look somewhere else.
53
53
  */
54
54
  export declare function registeredElsewhere(home: string, directory: string): RegisteredProject[];
55
+ export {};