@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
@@ -1,4 +1,5 @@
1
- import { REPLAY_PROGRAM_VERSION, asFlowName } from '@reticlehq/core';
1
+ import { ActionType, PredicateKind, QueryBy, REPLAY_PROGRAM_VERSION, asFlowName, defaultIsSensitiveKey, } from '@reticlehq/core';
2
+ import { clauseOfKind } from '@reticlehq/core';
2
3
  /**
3
4
  * What a session drove, saved as a replayable flow — without anybody asking for it.
4
5
  *
@@ -29,67 +30,62 @@ const segment = (value, max) => value
29
30
  * DOM-shaped claims are the weakest. Naming a flow after the strongest thing it proves is the same
30
31
  * judgement the honesty grade makes, reused where a human reads it.
31
32
  *
32
- * Returns '' when a step claims nothing nameable, and the caller keeps the old session-only name.
33
+ * Returns '' when a step claims nothing nameable.
33
34
  */
34
35
  function provedSlug(steps) {
35
36
  for (const step of steps) {
36
37
  const expect = step.expect;
37
38
  if (expect === undefined)
38
39
  continue;
39
- if (expect.signal !== undefined)
40
- return segment(expect.signal, 32);
41
- if (expect.net !== undefined) {
42
- const method = segment(expect.net.method ?? '', 6);
43
- const where = segment(expect.net.urlContains ?? '', 24);
40
+ // A predicate names its kind, so the slug reads the tree rather than probing a struct. The
41
+ // signal wins over the request for the same reason it always did: it is the app saying what it
42
+ // did, which is the strongest thing a name can carry.
43
+ const signal = clauseOfKind(expect, PredicateKind.SIGNAL);
44
+ if (signal?.name !== undefined)
45
+ return segment(signal.name, 32);
46
+ const netClause = clauseOfKind(expect, PredicateKind.NET);
47
+ if (netClause !== undefined) {
48
+ const method = segment(netClause.method ?? '', 6);
49
+ const where = segment(netClause.urlContains ?? '', 24);
44
50
  const joined = [method, where].filter((part) => '' !== part).join('-');
45
51
  if ('' !== joined)
46
52
  return joined;
47
53
  }
48
- if (expect.state !== undefined)
49
- return segment(expect.state.path, 32);
50
- if (expect.route?.pathname !== undefined)
51
- return segment(expect.route.pathname, 24);
52
- if (expect.text?.contains !== undefined)
53
- return segment(expect.text.contains, 24);
54
+ const state = clauseOfKind(expect, PredicateKind.STATE);
55
+ if (state !== undefined)
56
+ return segment(state.path, 32);
57
+ const route = clauseOfKind(expect, PredicateKind.ROUTE);
58
+ if (route?.pathname !== undefined)
59
+ return segment(route.pathname, 24);
60
+ const text = clauseOfKind(expect, PredicateKind.TEXT);
61
+ if (text?.contains !== undefined)
62
+ return segment(text.contains, 24);
54
63
  }
55
64
  return '';
56
65
  }
66
+ /** Every auto-saved flow's name starts here, which is what lets a merge tell them from hand-named ones. */
67
+ export const DRIVE_FLOW_PREFIX = 'drive-';
57
68
  /**
58
- * A stable name for what this session drove — led by what it proved.
69
+ * A readable name for a journey, led by WHY it was driven.
59
70
  *
60
- * Two properties, and the name used to have only the second. It has to be STABLE: teardown fires on
61
- * every socket close and a reconnecting tab keeps its id, so a name that moves leaves one journey
62
- * scattered across several files, each a partial copy of the others. And it has to be READABLE,
63
- * because `presenter-controls.ts` renders it straight into the replay button's `textContent` — so
64
- * the label a person actually sees was `drive-sdc991872-6d66-4adf-8780-f931c62905f9`, twice, for two
65
- * different journeys. A session id is a fact about the socket, not about the feature.
71
+ * The agent's own `intent` on the action is the best name there is — it is the sentence a person
72
+ * would use — so it leads when one was declared. Without one, what the journey proved names it (the
73
+ * strongest claim first), and the route follows so two journeys stay apart.
66
74
  *
67
- * So the claim leads, the route follows, and a SHORT session discriminator trails: two sessions that
68
- * prove the same thing on the same route stay separate files rather than overwriting each other,
69
- * which is the one thing the full id was buying that the claim cannot.
75
+ * No session id. It used to trail every name so two sessions never overwrote each other, which also
76
+ * made every session's drive of the same journey its own file. Two DIFFERENT journeys that land on
77
+ * one name are now told apart at save by their shape, and two copies of the SAME journey merge —
78
+ * see FlowStore.saveJourney.
70
79
  */
71
- export function driveFlowName(sessionId, route, steps = []) {
72
- const safe = sessionId.replace(/[^a-zA-Z0-9-]/g, '-').slice(0, 40);
73
- const proved = provedSlug(steps);
74
- // The route is part of the name, so two journeys in one session do not overwrite each other —
75
- // and so a re-drive of the same journey rewrites its own flow rather than adding a near-duplicate.
76
- const leg = (route ?? '')
77
- .replace(/[^a-zA-Z0-9]+/g, '-')
78
- .replace(/^-|-$/g, '')
79
- .slice(0, 24);
80
- // Minted here, not at the call site. This function is where a drive's flow name comes into
81
- // existence, and every character it can emit has just been forced through the safe-segment
82
- // replacements above — so this IS the validated boundary the brand is meant to be created at.
83
- // Returning `string` pushed a cast onto each caller, which is the hole `flowPath`'s parameter
84
- // type was added to close.
85
- // Nothing nameable was proved, so keep the name this function has always produced.
86
- if ('' === proved) {
87
- return asFlowName(0 === leg.length ? `drive-${safe}` : `drive-${safe}-${leg}`);
88
- }
89
- // Enough of the session to keep two of them apart, and little enough to read past.
90
- const discriminator = safe.replace(/-/g, '').slice(0, 8);
91
- const parts = ['drive', proved, ...(0 === leg.length ? [] : [leg]), discriminator];
92
- return asFlowName(parts.join('-'));
80
+ export function driveFlowName(route, steps = []) {
81
+ const intent = steps.find((step) => step.intent !== undefined)?.intent;
82
+ const leg = segment(route ?? '', 24);
83
+ const why = intent === undefined ? '' : segment(intent, 48);
84
+ const lead = '' !== why ? why : provedSlug(steps);
85
+ // Minted here: every character has just been forced through `segment`, so this IS the validated
86
+ // boundary the brand is meant to be created at.
87
+ const parts = [lead, ...('' !== why || 0 === leg.length ? [] : [leg])].filter((p) => '' !== p);
88
+ return asFlowName(`${DRIVE_FLOW_PREFIX}${0 === parts.length ? 'journey' : parts.join('-')}`);
93
89
  }
94
90
  /**
95
91
  * Turn this session's ambient tape into a flow, when it is worth one.
@@ -116,18 +112,39 @@ export function driveFlowName(sessionId, route, steps = []) {
116
112
  */
117
113
  function segmentsByRoute(steps) {
118
114
  const out = [];
115
+ let signedIn = false;
119
116
  for (const step of steps) {
120
117
  const last = out.at(-1);
121
- if (last !== undefined && last.route === step.route) {
118
+ const previous = last?.steps.at(-1);
119
+ // The same journey when the route is unchanged, or when the previous ACTION moved the app here
120
+ // — cart → shipping → payment is one flow, and cutting it left three fragments that each
121
+ // proved a page and none proved the journey. Not after a sign-in: that is the cut this exists
122
+ // for, and a login that navigates onward must still not open the next flow.
123
+ const caused = previous !== undefined && step.route !== undefined && previous.endPage === step.route;
124
+ if (last !== undefined && (last.route === step.route || (caused && !signedIn))) {
122
125
  last.steps.push(step);
126
+ signedIn ||= typesASecret(step);
123
127
  continue;
124
128
  }
129
+ signedIn = typesASecret(step);
125
130
  out.push({ ...(step.route === undefined ? {} : { route: step.route }), steps: [step] });
126
131
  }
127
132
  return out;
128
133
  }
134
+ /**
135
+ * A fill into a field the network channel would redact — the sign-in a journey must not carry.
136
+ * Read off the recorder's anchor args: a testid names the field by `value`, a role by `name`.
137
+ */
138
+ function typesASecret(step) {
139
+ const action = step.args['action'];
140
+ if (ActionType.FILL !== action && ActionType.TYPE !== action)
141
+ return false;
142
+ const by = step.args['by'];
143
+ const field = QueryBy.TESTID === by ? step.args['value'] : step.args['name'];
144
+ return 'string' === typeof field && defaultIsSensitiveKey(field);
145
+ }
129
146
  /** A flow per journey, so two journeys in one session are two regression tests rather than one. */
130
- export function driveFlowsFrom(sessionId, tape) {
147
+ export function driveFlowsFrom(tape) {
131
148
  if (tape === undefined || 0 === tape.steps.length)
132
149
  return { programs: [], outcome: {} };
133
150
  const programs = [];
@@ -139,7 +156,7 @@ export function driveFlowsFrom(sessionId, tape) {
139
156
  }
140
157
  const startPath = segment.route ?? tape.startPath;
141
158
  programs.push({
142
- name: driveFlowName(sessionId, startPath, segment.steps),
159
+ name: driveFlowName(startPath, segment.steps),
143
160
  version: REPLAY_PROGRAM_VERSION,
144
161
  // The route is recorder-internal and has no business on disk — `startPath` is where the
145
162
  // on-disk flow says the same thing, in the field replay actually reads.
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { writeFileAtomic } from '../project/fs/write-atomic.js';
2
3
  import { dirname } from 'node:path';
3
4
  import { reticleDirPaths } from '../project/dir/reticle-dir.js';
4
5
  import { RouteEnvelopeSchema } from './envelope.js';
@@ -54,8 +55,6 @@ export class EnvelopeStore {
54
55
  routes[route] = envelope;
55
56
  const body = `${JSON.stringify({ version: ENVELOPE_FILE_VERSION, routes }, null, 2)}\n`;
56
57
  // Atomic publish: write a temp sibling then rename, so a crash never leaves a half-written baseline.
57
- const tmp = `${this.#path}.tmp`;
58
- await this.#fs.writeFile(tmp, body);
59
- await this.#fs.rename(tmp, this.#path);
58
+ await writeFileAtomic(this.#fs, this.#path, body);
60
59
  }
61
60
  }
@@ -53,6 +53,27 @@ export interface JournalReader {
53
53
  * only ever about writes that never happened.
54
54
  */
55
55
  readWriteLoss?(): Promise<JournalWriteLoss | undefined>;
56
+ /**
57
+ * How big the event ledger is right now, and the ceiling it is measured against.
58
+ *
59
+ * Synchronous and cached: the size is already tracked for the cap, so reporting it costs a field
60
+ * read. Optional on the same reasoning as `readActions` -- a partial double declares no size,
61
+ * and a caller reads that as "not measured" rather than "empty".
62
+ *
63
+ * It is reported at all because nothing reported it. A repeating uncaught error wrote one
64
+ * session's ledger without limit, and neither the daemon nor
65
+ * `doctor` nor any health counter said a word; the user found out when the disk was full (#986).
66
+ * The cap that now bounds this is only half an answer -- a ledger silently approaching it is
67
+ * still a dev tool writing hard to somebody's repo directory.
68
+ */
69
+ ledger?(): JournalLedgerSize | undefined;
70
+ }
71
+ /** The event ledger's size on disk against the ceiling that will refuse it. */
72
+ export interface JournalLedgerSize {
73
+ /** Bytes of `events.jsonl` on disk. */
74
+ bytes: number;
75
+ /** The ceiling that refuses the next batch past it. */
76
+ capBytes: number;
56
77
  }
57
78
  interface JournalRecorderOptions {
58
79
  /** Injected elapsed-ms clock (never read from Date here — the clock-injection rule). */
@@ -0,0 +1,34 @@
1
+ /**
2
+ * What a project is willing to keep.
3
+ *
4
+ * The only answer `.reticle.json` used to offer a full directory was `journal: false` — turning off
5
+ * the thing the product is FOR, and, until the sweep was ungated, the one setting under which
6
+ * nothing was deleted either. The bounds themselves were four constants no user could reach.
7
+ *
8
+ * `0` is a real answer and means keep none, which the count bounds already do for free: a sweep that
9
+ * keeps zero directories deletes all of them. It is not the same as "unset", which is why every
10
+ * field is read explicitly rather than with `??` over a falsy value.
11
+ */
12
+ export interface RetainPolicy {
13
+ /** Session journal directories under `sessions/`. */
14
+ sessions: number;
15
+ /** Overlay diffs under `visual/`. Baselines are memory and are never counted here. */
16
+ visual: number;
17
+ /** Local copies of reports the outbox already carries. */
18
+ feedback: number;
19
+ /** The TOTAL the evidence tier may occupy, in bytes. Stated in the file as `budgetMb`. */
20
+ budgetBytes: number;
21
+ }
22
+ export declare const DEFAULT_RETAIN: RetainPolicy;
23
+ /**
24
+ * Read the `retain` block out of an already-parsed `.reticle.json`.
25
+ *
26
+ * Takes the record rather than a path: the config search belongs to the caller that already does it
27
+ * once at startup, and reaching for it from here would point the retention domain at the
28
+ * command-line surface for a single lookup.
29
+ *
30
+ * Never throws, and falls back PER FIELD. A project that mistypes one bound keeps the other three —
31
+ * the alternative silently reverts every bound the user did set, which is the same class of quiet
32
+ * disagreement the whole partition of `.reticle/` exists to prevent.
33
+ */
34
+ export declare function readRetainPolicy(config: Record<string, unknown> | undefined): RetainPolicy;
@@ -0,0 +1,44 @@
1
+ import { DEFAULT_DIFF_RETENTION, DEFAULT_EVIDENCE_BUDGET_BYTES, DEFAULT_FEEDBACK_RETENTION, DEFAULT_SESSION_RETENTION, } from './retention.js';
2
+ /** The key a project writes in `.reticle.json`. */
3
+ const RETAIN_KEY = 'retain';
4
+ const BYTES_PER_MB = 1024 * 1024;
5
+ export const DEFAULT_RETAIN = {
6
+ sessions: DEFAULT_SESSION_RETENTION,
7
+ visual: DEFAULT_DIFF_RETENTION,
8
+ feedback: DEFAULT_FEEDBACK_RETENTION,
9
+ budgetBytes: DEFAULT_EVIDENCE_BUDGET_BYTES,
10
+ };
11
+ /**
12
+ * A count a sweep can act on, or `undefined` for anything else.
13
+ *
14
+ * Negative and fractional are refused rather than coerced. "Keep -1 sessions" and "keep 1.5" are
15
+ * both a user saying something they did not mean, and a sweep that guesses at what they meant is
16
+ * deleting files on a guess.
17
+ */
18
+ function count(value) {
19
+ return 'number' === typeof value && Number.isInteger(value) && value >= 0 ? value : undefined;
20
+ }
21
+ /**
22
+ * Read the `retain` block out of an already-parsed `.reticle.json`.
23
+ *
24
+ * Takes the record rather than a path: the config search belongs to the caller that already does it
25
+ * once at startup, and reaching for it from here would point the retention domain at the
26
+ * command-line surface for a single lookup.
27
+ *
28
+ * Never throws, and falls back PER FIELD. A project that mistypes one bound keeps the other three —
29
+ * the alternative silently reverts every bound the user did set, which is the same class of quiet
30
+ * disagreement the whole partition of `.reticle/` exists to prevent.
31
+ */
32
+ export function readRetainPolicy(config) {
33
+ const raw = config?.[RETAIN_KEY];
34
+ if ('object' !== typeof raw || null === raw || Array.isArray(raw))
35
+ return DEFAULT_RETAIN;
36
+ const retain = raw;
37
+ const mb = count(retain['budgetMb']);
38
+ return {
39
+ sessions: count(retain['sessions']) ?? DEFAULT_RETAIN.sessions,
40
+ visual: count(retain['visual']) ?? DEFAULT_RETAIN.visual,
41
+ feedback: count(retain['feedback']) ?? DEFAULT_RETAIN.feedback,
42
+ budgetBytes: mb === undefined ? DEFAULT_RETAIN.budgetBytes : mb * BYTES_PER_MB,
43
+ };
44
+ }
@@ -16,9 +16,10 @@ export declare const DEFAULT_FEEDBACK_RETENTION = 20;
16
16
  * and feedback copies together.
17
17
  *
18
18
  * Every bound above this line counts things, and a count cannot see a size: twenty session
19
- * directories is twenty UNBOUNDED directories. A user's `.reticle/sessions` reached 8 GB with all
20
- * three counts being honoured the whole time, because one drive against a chatty app appends
21
- * response bodies and DOM text for as long as the drive lasts, and nothing was looking at the total.
19
+ * directories is twenty UNBOUNDED directories. A workspace in the field grew to multiple gigabytes
20
+ * with all three counts being honoured the whole time, because one drive against a chatty app
21
+ * appends response bodies and DOM text for as long as the drive lasts, and nothing was looking at
22
+ * the total.
22
23
  *
23
24
  * Why this number and not one an order of magnitude either side. An ordinary session journal is
24
25
  * hundreds of kilobytes to a few megabytes; a long drive against an app with large JSON responses
@@ -40,11 +41,11 @@ export declare const DEFAULT_EVIDENCE_BUDGET_BYTES: number;
40
41
  /**
41
42
  * One candidate entry inside a `.reticle/`, classified by the top-level directory it lives under.
42
43
  *
43
- * `under` is what decides the tier, and it is compared against `TRANSIENT_DIRS` — the same list the
44
- * workspace gitignore writes, guarded there as an exhaustive partition of what `.reticle/` holds.
45
- * Anything else is MEMORY: flows, capsules, baselines, the contract, the intent ledger. Those are
46
- * small, durable and the reason the directory exists, so they are never evicted, never counted, and
47
- * must keep being written however full the disk is.
44
+ * `under` is what decides the tier, and it is looked up in the workspace tier table's `durability`
45
+ * axis — never in the gitignore's, which answers a different question. Anything not EVIDENCE is
46
+ * MEMORY: flows, capsules, baselines, the contract, the intent ledger. Those are small, durable
47
+ * and the reason the directory exists, so they are never evicted, never counted, and must keep
48
+ * being written however full the disk is.
48
49
  */
49
50
  export interface TierEntry {
50
51
  /** Absolute path, and what is removed. */
@@ -73,8 +74,6 @@ export declare function selectPrunable(dirs: readonly DatedDir[], retention: num
73
74
  export interface RetentionOptions {
74
75
  /** How many session directories survive the count bound. */
75
76
  retention?: number;
76
- /** The TOTAL the evidence tier may occupy. */
77
- budgetBytes?: number;
78
77
  /**
79
78
  * The ids of sessions that are STILL OPEN. Neither bound may evict one.
80
79
  *
@@ -94,6 +93,11 @@ export interface RetentionOptions {
94
93
  * so a stat/rm failure on one dir (or a missing sessions/ dir) is swallowed, never crashing a session.
95
94
  * Mirrors RunStore's amortized pruning; run on daemon start and at the end of every session.
96
95
  *
96
+ * The COUNT bound only. This used to sweep the tier total as well, from when it was the single
97
+ * sweep both call sites used; `pruneWorkspace` is that single place now and runs the budget last,
98
+ * on purpose, so the leftover call here was a second walk of every evidence entry on every teardown
99
+ * — and it ran FIRST, sizing everything the counts were about to delete anyway.
100
+ *
97
101
  * An open session is excluded from BOTH bounds — see `RetentionOptions.live`.
98
102
  *
99
103
  * ponytail: excluded via `keep`, so an open session is not COUNTED toward the retention cap either.
@@ -1,7 +1,7 @@
1
1
  import { basename, join } from 'node:path';
2
2
  import { ReticleDir } from '@reticlehq/core';
3
3
  import { reticleDirPaths, visualDir } from '../../project/dir/reticle-dir.js';
4
- import { TRANSIENT_DIRS } from './workspace-gitignore.js';
4
+ import { EVIDENCE_DIRS } from './workspace-tiers.js';
5
5
  /** Keep at most this many session journals on disk; older ones are pruned by recency. */
6
6
  export const DEFAULT_SESSION_RETENTION = 20;
7
7
  /**
@@ -21,9 +21,10 @@ const PNG_SUFFIX = '.png';
21
21
  * and feedback copies together.
22
22
  *
23
23
  * Every bound above this line counts things, and a count cannot see a size: twenty session
24
- * directories is twenty UNBOUNDED directories. A user's `.reticle/sessions` reached 8 GB with all
25
- * three counts being honoured the whole time, because one drive against a chatty app appends
26
- * response bodies and DOM text for as long as the drive lasts, and nothing was looking at the total.
24
+ * directories is twenty UNBOUNDED directories. A workspace in the field grew to multiple gigabytes
25
+ * with all three counts being honoured the whole time, because one drive against a chatty app
26
+ * appends response bodies and DOM text for as long as the drive lasts, and nothing was looking at
27
+ * the total.
27
28
  *
28
29
  * Why this number and not one an order of magnitude either side. An ordinary session journal is
29
30
  * hundreds of kilobytes to a few megabytes; a long drive against an app with large JSON responses
@@ -60,7 +61,7 @@ function isOpenSession(entry, live) {
60
61
  * `selectPrunable` and `pruneByRecency` already split it.
61
62
  */
62
63
  export function selectOverBudget(entries, budgetBytes, live = NO_LIVE_SESSIONS) {
63
- const evidence = entries.filter((entry) => TRANSIENT_DIRS.includes(entry.under));
64
+ const evidence = entries.filter((entry) => EVIDENCE_DIRS.includes(entry.under));
64
65
  // Live sessions still COUNT — their bytes are on disk and the budget is about the disk. They are
65
66
  // only excluded from the eviction list, so a session over budget on its own is left alone rather
66
67
  // than deleted under its own writer.
@@ -78,8 +79,6 @@ export function selectOverBudget(entries, budgetBytes, live = NO_LIVE_SESSIONS)
78
79
  }
79
80
  return doomed;
80
81
  }
81
- /** Where refused feedback reports are copied. Named here because the writer lives in another area. */
82
- const FEEDBACK_SUBDIR = 'feedback';
83
82
  /**
84
83
  * Pure: given session dirs with their mtimes, the names to remove so only the `retention` most-recent
85
84
  * remain. Oldest-first. Exported for testing (no IO).
@@ -138,6 +137,11 @@ async function pruneByRecency(fs, dir, retention, keep = () => true) {
138
137
  * so a stat/rm failure on one dir (or a missing sessions/ dir) is swallowed, never crashing a session.
139
138
  * Mirrors RunStore's amortized pruning; run on daemon start and at the end of every session.
140
139
  *
140
+ * The COUNT bound only. This used to sweep the tier total as well, from when it was the single
141
+ * sweep both call sites used; `pruneWorkspace` is that single place now and runs the budget last,
142
+ * on purpose, so the leftover call here was a second walk of every evidence entry on every teardown
143
+ * — and it ran FIRST, sizing everything the counts were about to delete anyway.
144
+ *
141
145
  * An open session is excluded from BOTH bounds — see `RetentionOptions.live`.
142
146
  *
143
147
  * ponytail: excluded via `keep`, so an open session is not COUNTED toward the retention cap either.
@@ -148,9 +152,6 @@ export async function pruneSessions(fs, root, options = {}) {
148
152
  const live = options.live ?? NO_LIVE_SESSIONS;
149
153
  const retention = options.retention ?? DEFAULT_SESSION_RETENTION;
150
154
  await pruneByRecency(fs, reticleDirPaths(root).sessions, retention, (name) => !live.has(name));
151
- // The tier total is swept from here because this is the one sweep that runs on BOTH the daemon's
152
- // start path and the end of every session, and the end of a session is when the bytes arrive.
153
- await pruneEvidenceBudget(fs, root, options.budgetBytes ?? DEFAULT_EVIDENCE_BUDGET_BYTES, live);
154
155
  }
155
156
  /**
156
157
  * Bound `.reticle/visual/` — the largest thing in the workspace, and the only one with no delete
@@ -224,7 +225,7 @@ async function sizeOf(fs, path) {
224
225
  */
225
226
  async function collectEvidence(fs, root) {
226
227
  const entries = [];
227
- for (const under of TRANSIENT_DIRS) {
228
+ for (const under of EVIDENCE_DIRS) {
228
229
  const isVisual = ReticleDir.VISUAL_SUBDIR === under;
229
230
  const dirs = isVisual ? await visualDirs(fs, root) : [join(root, under)];
230
231
  for (const dir of dirs) {
@@ -287,5 +288,5 @@ export async function pruneEvidenceBudget(fs, root, budgetBytes = DEFAULT_EVIDEN
287
288
  * retention rule is the same rule whether the directory holds journals, diffs or reports.
288
289
  */
289
290
  export async function pruneFeedback(fs, root, retention = DEFAULT_FEEDBACK_RETENTION) {
290
- await pruneByRecency(fs, join(root, FEEDBACK_SUBDIR), retention);
291
+ await pruneByRecency(fs, join(root, ReticleDir.FEEDBACK_SUBDIR), retention);
291
292
  }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Everything Reticle prunes in ONE `.reticle/` workspace, in one place.
3
+ *
4
+ * Called from two moments and both matter: daemon start (the daemon's own tree) and session end
5
+ * (the tree that SESSION wrote into). Those are usually different directories — a globally
6
+ * registered daemon stands in `$HOME` — and the per-project one is where the bytes actually
7
+ * accumulate, so sweeping only the first swept the wrong tree for the common case.
8
+ *
9
+ * The four bounds were inlined at the call site and one of them was simply missing:
10
+ * `pruneEvidenceBudget` had been written, tested, and never called by anything. The three COUNT
11
+ * bounds were wired and the BYTE bound was not, which is the combination the budget's own header
12
+ * warns about -- "twenty session directories is twenty UNBOUNDED directories", beside a field
13
+ * report of a multi-gigabyte `.reticle/sessions` with all three counts honoured throughout.
14
+ *
15
+ * Counts and bytes answer different questions and neither substitutes for the other. A count says
16
+ * how MANY sessions are kept; it says nothing about how large one is allowed to get. A chatty app
17
+ * writes one enormous journal and never trips a count.
18
+ *
19
+ * Gathered into a function rather than left as four statements so that "what maintenance runs at
20
+ * startup" has one answer that can be TESTED. Inlined, the only way to check the budget was wired
21
+ * was to grep the call site for its name -- and a grep passes on a comment that quotes the code,
22
+ * which has already happened in this repository.
23
+ *
24
+ * Order: counts first, budget last. The counts are cheap and bounded; the budget walks and sizes
25
+ * what the counts leave behind, so running it second means it sizes less.
26
+ *
27
+ * NEVER THROWS. Maintenance must not be the reason a daemon fails to come up, and each prune below
28
+ * already swallows its own errors -- this only has to avoid adding a new way to fail.
29
+ */
30
+ import type { FileSystemPort } from '../../project/fs/fs-port.js';
31
+ import { type RetainPolicy } from './retain-policy.js';
32
+ /**
33
+ * What this sweep is allowed to keep. Every field optional, defaulting to the built-in bound.
34
+ *
35
+ * Partial rather than a whole `RetainPolicy` for two reasons that happen to agree: a test states
36
+ * only the bound it is exercising instead of writing half a gigabyte to disk, and a project's
37
+ * `.reticle.json` states only the bounds it cares about. Same reason the clock is injected
38
+ * everywhere else here — a bound that can only be exercised by producing the real quantity is a
39
+ * bound nothing exercises.
40
+ */
41
+ export type PruneWorkspaceOptions = Partial<RetainPolicy>;
42
+ export declare function pruneWorkspace(fs: FileSystemPort, root: string, live: ReadonlySet<string>, options?: PruneWorkspaceOptions): Promise<void>;
@@ -0,0 +1,14 @@
1
+ import { pruneEvidenceBudget, pruneFeedback, pruneSessions, pruneVisualDiffs, } from './retention.js';
2
+ import { DEFAULT_RETAIN } from './retain-policy.js';
3
+ export async function pruneWorkspace(fs, root, live, options = {}) {
4
+ const retain = { ...DEFAULT_RETAIN, ...options };
5
+ // Empty in the ordinary case (nothing has connected yet); this path also runs on a daemon that is
6
+ // already serving sessions, which is why `live` is threaded rather than assumed empty.
7
+ await pruneSessions(fs, root, { live, retention: retain.sessions });
8
+ // The largest thing in the workspace, and for a long time the only one with no delete path.
9
+ await pruneVisualDiffs(fs, root, retain.visual);
10
+ // Write-only local copies of reports the outbox already carries.
11
+ await pruneFeedback(fs, root, retain.feedback);
12
+ // LAST, and the one that was missing: the only bound that limits SIZE rather than count.
13
+ await pruneEvidenceBudget(fs, root, retain.budgetBytes, live);
14
+ }
@@ -1,15 +1,4 @@
1
1
  import type { FileSystemPort } from '../../project/fs/fs-port.js';
2
- /**
3
- * Local directories, without the trailing slash the ignore file wants.
4
- *
5
- * Exported because this is also the EVIDENCE tier: the per-session churn that retention is allowed
6
- * to delete. Read from here rather than restated next to the byte budget — the partition of what
7
- * `.reticle/` holds already exists, is guarded as exhaustive, and a second copy of it would be the
8
- * list that silently disagrees.
9
- */
10
- export declare const TRANSIENT_DIRS: readonly string[];
11
- /** Every name Reticle treats as machine-local, however it is spelled in the ignore file. */
12
- export declare const TRANSIENT_NAMES: readonly string[];
13
2
  /**
14
3
  * Write `.reticle/.gitignore` if it is not already there.
15
4
  *
@@ -1,60 +1,21 @@
1
1
  import { join } from 'node:path';
2
2
  import { ReticleDir } from '@reticlehq/core';
3
+ import { LOCAL_DIRS, LOCAL_FILES } from './workspace-tiers.js';
3
4
  /**
4
5
  * The `.reticle/` entries that are LOCAL state, and must never be committed by accident.
5
6
  *
6
- * Split from the durable half deliberately, and the split is the whole design: `contract.json`,
7
- * `flows/`, `intent.json` and `capsules/` are meant to be checked in — a flow that cannot be shared
8
- * is not a regression check anybody else can run — while everything below is per-machine churn.
7
+ * Read from the tier table rather than restated here: which half a name belongs to is one decision,
8
+ * and a second copy of it is the list that silently disagrees. This file asks the table exactly one
9
+ * of its two questions — `sharing` — and has no opinion at all about what retention may delete.
9
10
  *
10
11
  * The reason this matters beyond tidiness: a session journal carries URLs, request and response
11
12
  * bodies, and DOM text from the app under test. `.reticle/` is created by the daemon rather than by
12
13
  * the user, so it arrives untracked and unexplained, and one `git add -A` puts an app's traffic into
13
14
  * a shared repository. Reticle created the directory; the ignore for it is Reticle's to write.
14
15
  */
15
- /**
16
- * The local feedback copies — `.reticle/feedback/`.
17
- *
18
- * Named here rather than imported: the directory is spelled inline where it is written, in files
19
- * another agent owns, and the ignore for a directory Reticle creates is Reticle's to write whether
20
- * or not that name has been hoisted yet.
21
- */
22
- const FEEDBACK_SUBDIR = 'feedback';
23
- /**
24
- * Local directories, without the trailing slash the ignore file wants.
25
- *
26
- * Exported because this is also the EVIDENCE tier: the per-session churn that retention is allowed
27
- * to delete. Read from here rather than restated next to the byte budget — the partition of what
28
- * `.reticle/` holds already exists, is guarded as exhaustive, and a second copy of it would be the
29
- * list that silently disagrees.
30
- */
31
- export const TRANSIENT_DIRS = [
32
- ReticleDir.SESSIONS_SUBDIR,
33
- ReticleDir.RUNS_SUBDIR,
34
- ReticleDir.VISUAL_SUBDIR,
35
- // Write-only: a local copy of what was already sent. The outbox is the record.
36
- FEEDBACK_SUBDIR,
37
- ];
38
- /** Local files. Exported with the directories as `TRANSIENT_NAMES`, which the partition guard reads. */
39
- const TRANSIENT_FILES = [
40
- ReticleDir.PROJECT_FILE,
41
- ReticleDir.AMBIENT_FILE,
42
- ReticleDir.ENVELOPES_FILE,
43
- ReticleDir.FLAKE_FILE,
44
- ReticleDir.TIERS_FILE,
45
- // The user's own record of what Reticle did for them, on THIS machine.
46
- ReticleDir.IMPACT_FILE,
47
- // This machine's conversation with the server. Its own doc comment says why committing it is
48
- // harmful: one machine's pull cursor makes every other machine skip what it has not seen.
49
- ReticleDir.CLOUD_STATE_FILE,
50
- // Triage pulled back from the dashboard — a cache of somebody's decisions, re-pullable at will.
51
- ReticleDir.ISSUES_FILE,
52
- ];
53
- /** Every name Reticle treats as machine-local, however it is spelled in the ignore file. */
54
- export const TRANSIENT_NAMES = [...TRANSIENT_DIRS, ...TRANSIENT_FILES];
55
16
  const TRANSIENT = [
56
- ...TRANSIENT_DIRS.map((dir) => `${dir}/`),
57
- ...TRANSIENT_FILES,
17
+ ...LOCAL_DIRS.map((dir) => `${dir}/`),
18
+ ...LOCAL_FILES,
58
19
  '*.log',
59
20
  '*.tmp',
60
21
  ];
@@ -0,0 +1,52 @@
1
+ /**
2
+ * May retention delete this to reclaim disk?
3
+ *
4
+ * MEMORY is why `.reticle/` exists — what a flow replays from, what a deviation is measured
5
+ * against. It is small, it is never evicted, and it must keep being written however full the disk
6
+ * is. EVIDENCE is what one drive left behind: journals, run artifacts, pixel diffs. It is large, it
7
+ * ages out, and losing the oldest of it costs nothing anybody can name.
8
+ */
9
+ export declare const Durability: {
10
+ readonly MEMORY: "memory";
11
+ readonly EVIDENCE: "evidence";
12
+ };
13
+ export type Durability = (typeof Durability)[keyof typeof Durability];
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 declare const Sharing: {
22
+ readonly COMMIT: "commit";
23
+ readonly LOCAL: "local";
24
+ };
25
+ export type Sharing = (typeof Sharing)[keyof typeof Sharing];
26
+ /** What one `.reticle/` entry is, on both axes at once. */
27
+ export interface WorkspaceTier {
28
+ readonly durability: Durability;
29
+ readonly sharing: Sharing;
30
+ /** A directory is ignored as `name/` and swept entry by entry; a file is neither. */
31
+ readonly isDirectory: boolean;
32
+ }
33
+ /**
34
+ * Every top-level name `.reticle/` can hold, classified on both axes.
35
+ *
36
+ * One list used to answer both questions with one answer, and they are not the same question.
37
+ * `capsules/` is committed — a fail-to-pass capsule a teammate cannot replay is not a regression
38
+ * check — AND it is evidence a drive left behind, which grows without bound. That cell did not
39
+ * exist, so the byte budget, which read the gitignore's list, structurally could not see capsules
40
+ * however large they got. Splitting the axes is what makes that sayable; it does not by itself say
41
+ * it, and `capsules/` stays MEMORY here until the budget is measured against it.
42
+ *
43
+ * Declaration order is the order the ignore file lists them, so the file a user already has on disk
44
+ * and the one written today are the same file.
45
+ */
46
+ export declare const WORKSPACE_TIERS: Readonly<Record<string, WorkspaceTier>>;
47
+ /** Machine-local directories, without the trailing slash the ignore file wants. */
48
+ export declare const LOCAL_DIRS: readonly string[];
49
+ /** Machine-local files. */
50
+ export declare const LOCAL_FILES: readonly string[];
51
+ /** The directories retention sweeps, ignored or not. The byte budget and the count bounds read this. */
52
+ export declare const EVIDENCE_DIRS: readonly string[];