@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
@@ -30,7 +30,7 @@
30
30
  * Every dependency is injected — the clock, the fetch, the reads and writes — because this is the
31
31
  * one piece of Reticle that talks to a network AND to a disk, and it must be provable without either.
32
32
  */
33
- import { ReticleDir } from '@reticlehq/core';
33
+ import { FlowErrorCode, ReticleDir, SYNC_BATCH_LIMITS } from '@reticlehq/core';
34
34
  import { hashPayload } from './sync-hash.js';
35
35
  /** The server's own doors. Kept beside the code that calls them, like the other cloud paths. */
36
36
  const SYNC_PATH = '/v1/sync';
@@ -46,6 +46,10 @@ const DERIVED_RECORDS = [
46
46
  { kind: 'impact', file: ReticleDir.IMPACT_FILE },
47
47
  { kind: 'flake', file: ReticleDir.FLAKE_FILE },
48
48
  { kind: 'intent', file: ReticleDir.INTENT_FILE },
49
+ // How each page normally behaves, and how strong each flow's checks are. Kept only here, they
50
+ // cannot tell a server a page that drifted from one that always behaved that way.
51
+ { kind: 'envelopes', file: ReticleDir.ENVELOPES_FILE },
52
+ { kind: 'assertion-tiers', file: ReticleDir.TIERS_FILE },
49
53
  ];
50
54
  const asJson = (text) => {
51
55
  try {
@@ -56,6 +60,57 @@ const asJson = (text) => {
56
60
  }
57
61
  };
58
62
  const isRecord = (v) => 'object' === typeof v && null !== v;
63
+ /** A list of numbers, or undefined when the server did not send a list at all. */
64
+ const numbersIn = (v) => Array.isArray(v) ? v.filter((n) => 'number' === typeof n) : undefined;
65
+ /** A numeric field of an artifact, or undefined when it has none. */
66
+ const numberAt = (v, field) => {
67
+ const n = isRecord(v) ? v[field] : undefined;
68
+ return 'number' === typeof n ? n : undefined;
69
+ };
70
+ /** The name a person knows a flow by, falling back to its position. */
71
+ const flowName = (flow, index) => {
72
+ const name = isRecord(flow) ? flow['name'] : undefined;
73
+ return 'string' === typeof name && name.length > 0 ? name : `flow #${String(index)}`;
74
+ };
75
+ const FLOW_VERSION_FIX = 'update the platform or remove `compare` from the flow';
76
+ const RUN_VERSION_FIX = 'update the platform';
77
+ /** The server's per-part answer: accepted count and rejection list, each read defensively. */
78
+ function partResult(body, part) {
79
+ const raw = isRecord(body[part]) ? body[part] : {};
80
+ const accepted = 'number' === typeof raw['accepted'] ? raw['accepted'] : 0;
81
+ const rejected = (Array.isArray(raw['rejected']) ? raw['rejected'] : [])
82
+ .filter(isRecord)
83
+ .map((r) => ({
84
+ index: 'number' === typeof r['index'] ? r['index'] : -1,
85
+ reason: 'string' === typeof r['reason'] ? r['reason'] : 'no reason given',
86
+ code: r['code'],
87
+ version: r['version'],
88
+ }));
89
+ return { accepted, rejected };
90
+ }
91
+ /**
92
+ * Split runs into requests bounded by count and by serialized size. A run bigger than the byte
93
+ * bound on its own still goes, alone: it cannot be split, and holding it back would hide it.
94
+ */
95
+ function batchRuns(runs) {
96
+ const batches = [];
97
+ let current = [];
98
+ let bytes = 0;
99
+ for (const run of runs) {
100
+ const size = Buffer.byteLength(JSON.stringify(run.payload));
101
+ const full = current.length >= SYNC_BATCH_LIMITS.MAX_RUNS || bytes + size > SYNC_BATCH_LIMITS.MAX_BYTES;
102
+ if (current.length > 0 && full) {
103
+ batches.push(current);
104
+ current = [];
105
+ bytes = 0;
106
+ }
107
+ current.push(run);
108
+ bytes += size;
109
+ }
110
+ if (current.length > 0)
111
+ batches.push(current);
112
+ return batches;
113
+ }
59
114
  /**
60
115
  * Run one cycle. Never throws: the local record is already safe on disk, and a sync that can take
61
116
  * down the thing it is backing up is worse than no sync.
@@ -68,6 +123,10 @@ export async function runSyncCycle(deps) {
68
123
  flowsSent: 0,
69
124
  capsulesSent: 0,
70
125
  derivedSent: [],
126
+ refused: [],
127
+ held: [],
128
+ notRetried: [],
129
+ setsNotRetried: [],
71
130
  pulled: 0,
72
131
  morePending: false,
73
132
  };
@@ -99,66 +158,237 @@ export async function runSyncCycle(deps) {
99
158
  * something the server does not have. Silent data loss is the one failure this protocol must
100
159
  * not have, and an unreadable answer has to mean "it knows nothing", never "it knows this".
101
160
  */
102
- const known = new Set(Array.isArray(held.knownRunIds)
103
- ? held.knownRunIds.filter((id) => 'string' === typeof id)
104
- : []);
161
+ const onlyStrings = (value) => Array.isArray(value) ? value.filter((id) => 'string' === typeof id) : [];
162
+ /*
163
+ * The server may answer "here are the ids I hold, and there are more than I listed". Read as the
164
+ * whole truth, that short list made every run past the server's page look unsent, so a project
165
+ * with enough runs to truncate it re-uploaded its history on every single cycle, forever. The
166
+ * field was declared on this response and read nowhere.
167
+ *
168
+ * Only then is the machine's own record consulted, and it is subject to the same rule as the
169
+ * server's: anything that is not a list of strings reads as "nothing is known".
170
+ */
171
+ const known = new Set([
172
+ ...onlyStrings(held.knownRunIds),
173
+ ...(true === held.truncated ? onlyStrings(deps.state.sentRunIds) : []),
174
+ ]);
105
175
  // Same rule for the hashes: only a string can equal a hash we computed, so anything else reads
106
176
  // as "unknown" and the record is sent once. Re-sending costs a request; skipping costs the data.
107
177
  const hashes = isRecord(held.stateHashes) ? held.stateHashes : {};
108
- // 2. SEND — only what the server does not already have.
178
+ /*
179
+ * What this platform reads. `accepts` says so outright; a platform that predates it still lists
180
+ * every record kind it knows in `stateHashes`, null when it holds nothing. A kind it does not
181
+ * list is one it does not know, and it will never answer with a hash for it: sending on "hash
182
+ * differs" then re-sent that record, and every flow and capsule riding with it, on every cycle.
183
+ */
184
+ const accepts = isRecord(held.accepts) ? held.accepts : {};
185
+ const acceptedKinds = new Set(Array.isArray(accepts['derived']) ? onlyStrings(accepts['derived']) : Object.keys(hashes));
186
+ const flowVersions = numbersIn(accepts['flowVersions']);
187
+ const runVersions = numbersIn(accepts['runVersions']);
188
+ const heldBack = [];
189
+ /** Held runs and flows mean the dashboard is missing something; unknown record kinds do not. */
190
+ let heldArtifacts = false;
191
+ // 2. SEND — only what the server does not already have, and only what it can read.
109
192
  const allRuns = deps.source.runs();
110
- const unsent = allRuns.filter((r) => !known.has(r.runId));
111
- const derivedSent = [];
193
+ const acceptsHash = hashPayload(held.accepts ?? null);
194
+ const priorRefusals = isRecord(deps.state.refusedRuns) ? deps.state.refusedRuns : {};
195
+ const refusedRuns = {};
196
+ const notRetried = [];
197
+ const unsent = allRuns.filter((run) => {
198
+ if (known.has(run.runId))
199
+ return false;
200
+ const prior = priorRefusals[run.runId];
201
+ const unchanged = prior !== undefined &&
202
+ 'string' === typeof prior.reason &&
203
+ prior.acceptsHash === acceptsHash &&
204
+ prior.payloadHash === hashPayload(run.payload);
205
+ if (!unchanged)
206
+ return true;
207
+ refusedRuns[run.runId] = prior;
208
+ notRetried.push({ runId: run.runId, reason: prior.reason });
209
+ return false;
210
+ });
211
+ const sendable = unsent.filter((run) => {
212
+ const version = numberAt(run.payload, 'schemaVersion');
213
+ if (runVersions === undefined || version === undefined || runVersions.includes(version))
214
+ return true;
215
+ heldBack.push(`this platform reads run versions ${runVersions.join(', ')}; run ${run.runId} is version ${String(version)} — ${RUN_VERSION_FIX}`);
216
+ heldArtifacts = true;
217
+ return false;
218
+ });
219
+ const derivedOffered = [];
220
+ const unsupported = [];
112
221
  const bundle = {};
113
- if (unsent.length > 0)
114
- bundle['runs'] = unsent.map((r) => r.payload);
115
222
  for (const { kind } of DERIVED_RECORDS) {
116
223
  const payload = deps.source.derived(kind);
117
224
  if (payload === undefined)
118
225
  continue;
226
+ if (!acceptedKinds.has(kind)) {
227
+ unsupported.push(kind);
228
+ continue;
229
+ }
119
230
  // The one comparison the whole protocol rests on. Same hash, do not send it.
120
231
  if (hashPayload(payload) === hashes[kind])
121
232
  continue;
122
233
  bundle[kind] = payload;
123
- derivedSent.push(kind);
234
+ derivedOffered.push(kind);
124
235
  }
125
- // Flows are small and upserted by name, so they ride along whenever anything else does rather
126
- // than earning a round trip of their own.
127
- const ridesAlong = bundle['runs'] !== undefined || derivedSent.length > 0;
128
- const flows = ridesAlong ? deps.source.flows() : [];
129
- if (flows.length > 0)
236
+ if (unsupported.length > 0)
237
+ heldBack.push(`this platform does not accept ${unsupported.join(', ')} yet`);
238
+ // Flows and capsules are upserted by name/id, so the whole set goes whenever it differs from the
239
+ // set the platform last answered for, and rides along with anything else being sent.
240
+ const flows = deps.source.flows().filter((flow, index) => {
241
+ const version = numberAt(flow, 'version');
242
+ if (flowVersions === undefined || version === undefined || flowVersions.includes(version))
243
+ return true;
244
+ heldBack.push(`this platform reads flow versions ${flowVersions.join(', ')}; ${flowName(flow, index)} is version ${String(version)} — ${FLOW_VERSION_FIX}`);
245
+ heldArtifacts = true;
246
+ return false;
247
+ });
248
+ const capsulesAll = deps.source.capsules();
249
+ const flowsHash = hashPayload(flows);
250
+ const capsulesHash = hashPayload(capsulesAll);
251
+ const priorSets = isRecord(deps.state.refusedSets) ? deps.state.refusedSets : {};
252
+ const setsNotRetried = [];
253
+ const refusedSets = {};
254
+ /** A set is due when it changed, or when it was refused and the platform now reads more. */
255
+ const due = (part, hash, size, sentHash) => {
256
+ if (0 === size)
257
+ return false;
258
+ const prior = priorSets[part];
259
+ if (prior !== undefined && prior.hash === hash) {
260
+ if (prior.acceptsHash !== acceptsHash)
261
+ return true;
262
+ refusedSets[part] = prior;
263
+ setsNotRetried.push({ part, count: prior.count, reason: prior.reason });
264
+ return false;
265
+ }
266
+ return hash !== sentHash;
267
+ };
268
+ const flowsChanged = due('flow', flowsHash, flows.length, deps.state.sentFlowsHash);
269
+ const capsulesChanged = due('capsule', capsulesHash, capsulesAll.length, deps.state.sentCapsulesHash);
270
+ const ridesAlong = sendable.length > 0 || derivedOffered.length > 0 || flowsChanged || capsulesChanged;
271
+ if (ridesAlong && flows.length > 0)
130
272
  bundle['flows'] = flows;
131
- // Same gate as flows: only when something else is already going. A cycle that sent capsules and
132
- // nothing else would wake the server on every tick of an idle machine.
133
- const capsules = ridesAlong ? deps.source.capsules() : [];
273
+ const capsules = ridesAlong ? capsulesAll : [];
134
274
  if (capsules.length > 0)
135
275
  bundle['capsules'] = capsules;
136
276
  let runsSent = 0;
137
277
  let flowsSent = 0;
138
278
  let capsulesSent = 0;
139
- let runsRejected = [];
140
- if (Object.keys(bundle).length > 0) {
141
- const pushed = await call(SYNC_PATH, { method: 'POST', body: JSON.stringify(bundle) });
279
+ const runsRejected = [];
280
+ const refused = [];
281
+ const derivedSent = [];
282
+ let pushError;
283
+ /*
284
+ * Runs go up in bounded batches; everything else rides in the first. One request used to carry
285
+ * the whole backlog, and a backlog over the platform's body limit was refused whole, forever.
286
+ */
287
+ const batches = batchRuns(sendable);
288
+ const requests = Object.keys(bundle).length > 0 || batches.length > 0 ? Math.max(1, batches.length) : 0;
289
+ let offset = 0;
290
+ for (let i = 0; i < requests; i += 1) {
291
+ const batch = batches[i] ?? [];
292
+ const body = 0 === i ? { ...bundle } : {};
293
+ if (batch.length > 0)
294
+ body['runs'] = batch.map((r) => r.payload);
295
+ const pushed = await call(SYNC_PATH, { method: 'POST', body: JSON.stringify(body) });
142
296
  if (200 !== pushed.status) {
143
- const error = `sync ${String(pushed.status)}: ${pushed.text.slice(0, 200)}`;
144
- deps.sink.writeState({ ...nextState, lastError: error });
145
- return { ...empty, error };
297
+ pushError = `sync ${String(pushed.status)}: ${pushed.text.slice(0, 200)}`;
298
+ break;
299
+ }
300
+ const answer = isRecord(pushed.json) ? pushed.json : {};
301
+ const runs = partResult(answer, 'runs');
302
+ runsSent += runs.accepted;
303
+ // Accepted means the server has it. A rejected run was refused by index, so it is exactly as
304
+ // unsent as it was before and must never be remembered as delivered.
305
+ const refusedHere = new Map(runs.rejected.map((r) => [r.index, r.reason]));
306
+ batch.forEach((run, index) => {
307
+ const reason = refusedHere.get(index);
308
+ if (reason === undefined)
309
+ known.add(run.runId);
310
+ else
311
+ refusedRuns[run.runId] = { reason, payloadHash: hashPayload(run.payload), acceptsHash };
312
+ });
313
+ runsRejected.push(...runs.rejected.map((r) => ({ index: r.index + offset, reason: r.reason })));
314
+ offset += batch.length;
315
+ if (0 !== i)
316
+ continue;
317
+ // Absent means the server said nothing about a part — an older one that does not know the
318
+ // field. Read as zero accepted, never as an error: an added field must not break a sync.
319
+ const flowsPart = partResult(answer, 'flows');
320
+ const capsulesPart = partResult(answer, 'capsules');
321
+ flowsSent = flowsPart.accepted;
322
+ capsulesSent = capsulesPart.accepted;
323
+ for (const r of flowsPart.rejected) {
324
+ const name = flowName(flows[r.index], r.index);
325
+ refused.push({
326
+ part: 'flow',
327
+ reason: FlowErrorCode.WRONG_VERSION === r.code && 'number' === typeof r.version
328
+ ? `${name} is version ${String(r.version)} — ${FLOW_VERSION_FIX}`
329
+ : `flow ${name}: ${r.reason}`,
330
+ });
331
+ }
332
+ for (const r of capsulesPart.rejected)
333
+ refused.push({ part: 'capsule', reason: `capsule #${String(r.index)}: ${r.reason}` });
334
+ const firstOf = (part) => refused.find((r) => r.part === part)?.reason ?? 'no reason given';
335
+ if (flowsPart.rejected.length > 0)
336
+ refusedSets.flow = {
337
+ hash: flowsHash,
338
+ acceptsHash,
339
+ count: flowsPart.rejected.length,
340
+ reason: firstOf('flow'),
341
+ };
342
+ if (capsulesPart.rejected.length > 0)
343
+ refusedSets.capsule = {
344
+ hash: capsulesHash,
345
+ acceptsHash,
346
+ count: capsulesPart.rejected.length,
347
+ reason: firstOf('capsule'),
348
+ };
349
+ // `state` has a key per record sent: "accepted", or the message that refused it.
350
+ const state = isRecord(answer['state']) ? answer['state'] : {};
351
+ for (const kind of derivedOffered) {
352
+ const verdict = state[kind];
353
+ if ('accepted' === verdict)
354
+ derivedSent.push(kind);
355
+ else if ('string' === typeof verdict)
356
+ refused.push({ part: kind, reason: `${kind}: ${verdict}` });
146
357
  }
147
- const body = isRecord(pushed.json) ? pushed.json : {};
148
- const runs = isRecord(body['runs']) ? body['runs'] : {};
149
- const flowsPart = isRecord(body['flows']) ? body['flows'] : {};
150
- // Absent means the server said nothing about capsules — an older one that does not know the
151
- // field. Read as zero accepted, never as an error: an added field must not break a sync that
152
- // was otherwise fine, and the push's own status code is what reports a real refusal.
153
- const capsulesPart = isRecord(body['capsules']) ? body['capsules'] : {};
154
- runsSent = 'number' === typeof runs['accepted'] ? runs['accepted'] : 0;
155
- flowsSent = 'number' === typeof flowsPart['accepted'] ? flowsPart['accepted'] : 0;
156
- capsulesSent = 'number' === typeof capsulesPart['accepted'] ? capsulesPart['accepted'] : 0;
157
- runsRejected = Array.isArray(runs['rejected'])
158
- ? runs['rejected']
159
- : [];
358
+ }
359
+ if (requests > 0 && pushError === undefined) {
160
360
  nextState.lastPushAt = deps.now();
361
+ // The platform answered for these sets, refusals included (reported above, and not re-offered
362
+ // until the set changes — the same rule as a refused run).
363
+ if (ridesAlong && flows.length > 0)
364
+ nextState.sentFlowsHash = flowsHash;
365
+ if (capsules.length > 0)
366
+ nextState.sentCapsulesHash = capsulesHash;
161
367
  }
368
+ nextState.refusedRuns = refusedRuns;
369
+ nextState.refusedSets = refusedSets;
370
+ if (pushError !== undefined) {
371
+ nextState.sentRunIds = allRuns.map((r) => r.runId).filter((id) => known.has(id));
372
+ deps.sink.writeState({ ...nextState, lastError: pushError });
373
+ return {
374
+ ...empty,
375
+ runsSent,
376
+ runsRejected,
377
+ flowsSent,
378
+ capsulesSent,
379
+ derivedSent,
380
+ refused,
381
+ held: heldBack,
382
+ notRetried,
383
+ error: pushError,
384
+ };
385
+ }
386
+ /*
387
+ * Bounded by the runs still on disk rather than by a cap. Retention already decides how many
388
+ * runs a workspace keeps, and an id for a run nobody holds any more cannot stop an upload that
389
+ * will never be attempted — so the record follows the artifacts and needs no number of its own.
390
+ */
391
+ nextState.sentRunIds = allRuns.map((r) => r.runId).filter((id) => known.has(id));
162
392
  // 3. COLLECT — always, even when there was nothing to send.
163
393
  const query = nextState.cursor === undefined ? '' : `?since=${encodeURIComponent(nextState.cursor)}`;
164
394
  const pull = await call(`${SYNC_PULL_PATH}${query}`);
@@ -174,6 +404,9 @@ export async function runSyncCycle(deps) {
174
404
  capsulesSent,
175
405
  derivedSent,
176
406
  runsRejected,
407
+ refused,
408
+ held: heldBack,
409
+ notRetried,
177
410
  error,
178
411
  };
179
412
  }
@@ -196,15 +429,22 @@ export async function runSyncCycle(deps) {
196
429
  if ('string' === typeof pulled.cursor)
197
430
  nextState.cursor = pulled.cursor;
198
431
  nextState.lastPullAt = deps.now();
199
- delete nextState.lastError;
200
- deps.sink.writeState(nextState);
201
- return {
202
- ok: true,
432
+ const report = {
433
+ // Anything refused, or any artifact held back, means the dashboard is missing something.
434
+ ok: 0 === runsRejected.length &&
435
+ 0 === refused.length &&
436
+ 0 === notRetried.length &&
437
+ 0 === setsNotRetried.length &&
438
+ !heldArtifacts,
203
439
  runsSent,
204
440
  runsRejected,
205
441
  flowsSent,
206
442
  capsulesSent,
207
443
  derivedSent,
444
+ refused,
445
+ held: heldBack,
446
+ notRetried,
447
+ setsNotRetried,
208
448
  pulled: decisions.length,
209
449
  morePending: true === pulled.more,
210
450
  /*
@@ -216,6 +456,16 @@ export async function runSyncCycle(deps) {
216
456
  0 === deps.source.flows().length &&
217
457
  DERIVED_RECORDS.every(({ kind }) => deps.source.derived(kind) === undefined),
218
458
  };
459
+ /*
460
+ * A refusal is kept as the last error rather than cleared by a completed exchange: it is what
461
+ * `reticle whoami` shows, and "the transfer worked" is not "the dashboard has it".
462
+ */
463
+ if (report.ok)
464
+ delete nextState.lastError;
465
+ else
466
+ nextState.lastError = problemsOf(report).join('; ');
467
+ deps.sink.writeState(nextState);
468
+ return report;
219
469
  }
220
470
  catch (error) {
221
471
  // A network that is down is not an error condition for a local-first tool; it is Tuesday.
@@ -268,9 +518,10 @@ export function describeSync(report) {
268
518
  * to send, 3 rejected" — something was very much sent, and the reader is told both that it was
269
519
  * not and nothing about why.
270
520
  */
521
+ const rejectedCount = report.runsRejected.length + report.refused.length;
271
522
  const push = sent.length > 0
272
523
  ? `sent ${sent.join(' + ')}`
273
- : report.runsRejected.length > 0
524
+ : rejectedCount > 0
274
525
  ? 'nothing accepted'
275
526
  : true === report.localIsEmpty
276
527
  ? // Not the same statement as "nothing to send", which describes a repo that is simply up
@@ -281,15 +532,32 @@ export function describeSync(report) {
281
532
  const pull = 0 === report.pulled
282
533
  ? ''
283
534
  : `, pulled ${String(report.pulled)} decision(s)${report.morePending ? ' (more waiting)' : ''}`;
284
- /*
285
- * One reason, not a count. A rejection count tells somebody they have a problem and nothing about
286
- * which problem — and these arrive from a BACKGROUND daemon, so the summary line is often the only
287
- * place anybody ever sees it. Rejections in one cycle almost always share a cause (a version skew
288
- * refuses every payload the same way), so the first reason plus the count is the whole story
289
- * without printing a line per run; `reticle sync` still lists them all.
290
- */
291
- const bad = 0 === report.runsRejected.length
292
- ? ''
293
- : `, ${String(report.runsRejected.length)} rejected — ${report.runsRejected[0]?.reason ?? 'no reason given'}`;
294
- return `${push}${pull}${bad}`;
535
+ return `${push}${pull}${problemsOf(report)
536
+ .map((problem) => `, ${problem}`)
537
+ .join('')}`;
538
+ }
539
+ /**
540
+ * What went wrong, one phrase per kind of problem.
541
+ *
542
+ * One reason, not a count. A rejection count tells somebody they have a problem and nothing about
543
+ * which problem — and these arrive from a BACKGROUND daemon, so the summary line is often the only
544
+ * place anybody ever sees it. Rejections in one cycle almost always share a cause (a version skew
545
+ * refuses every payload the same way), so the first reason plus the count is the whole story
546
+ * without printing a line per item; `reticle sync` still lists them all.
547
+ */
548
+ function problemsOf(report) {
549
+ const problems = [];
550
+ const count = report.runsRejected.length + report.refused.length;
551
+ if (count > 0) {
552
+ const first = report.runsRejected[0]?.reason ?? report.refused[0]?.reason ?? 'no reason given';
553
+ problems.push(`${String(count)} rejected — ${first}`);
554
+ }
555
+ const firstRefusal = report.notRetried[0];
556
+ if (firstRefusal !== undefined)
557
+ problems.push(`refused, not retried: ${String(report.notRetried.length)} run(s) (${firstRefusal.reason})`);
558
+ for (const set of report.setsNotRetried)
559
+ problems.push(`refused, not retried: ${String(set.count)} ${set.part}(s) (${set.reason})`);
560
+ if (report.held.length > 0)
561
+ problems.push(`not sent: ${report.held.join('; ')}`);
562
+ return problems;
295
563
  }
@@ -45,6 +45,19 @@ export interface SyncDaemon {
45
45
  */
46
46
  nudge: () => void;
47
47
  stop: () => void;
48
+ /**
49
+ * Stop, but send what is still owed first.
50
+ *
51
+ * `stop()` cancels the pending timer, and the last thing a session writes is the run artifact the
52
+ * whole drive produced — so a daemon asked to close moments after a verdict dropped the one push
53
+ * anybody was waiting on. Measured against app.reticle.sh: a drive wrote its run, the daemon
54
+ * exited, and the dashboard's newest row stayed three weeks old.
55
+ *
56
+ * Bounded, because shutdown is not allowed to hang: an in-flight cycle is waited out only until
57
+ * the deadline, and a cycle that cannot finish in time is simply the interval's problem next
58
+ * time — the artifact is still on disk and the next daemon for this project sends it.
59
+ */
60
+ flush: () => Promise<void>;
48
61
  }
49
62
  /**
50
63
  * Start the loop. Safe to call for an UNLINKED project: it resolves the link on every tick and does
@@ -54,6 +54,21 @@ const FIRST_CYCLE_DELAY_MS = 5_000;
54
54
  * written just after, so cycling on the very first byte would ship a bundle that is missing them.
55
55
  */
56
56
  const NUDGE_DELAY_MS = 1_500;
57
+ /**
58
+ * How long a flush waits for a cycle already in flight before running its own.
59
+ *
60
+ * Shutdown budget, not a sync budget: every call inside a cycle already carries its own timeout, so
61
+ * this only bounds the WAIT. Generous enough for an ordinary cycle to finish, short enough that a
62
+ * stalled one cannot hold a daemon open.
63
+ */
64
+ const FLUSH_WAIT_MS = 3_000;
65
+ const FLUSH_POLL_MS = 25;
66
+ /** Whether a cycle landed or collected anything — what earns the fast interval and a log line. */
67
+ const movedAnything = (report) => report.runsSent > 0 ||
68
+ report.flowsSent > 0 ||
69
+ report.capsulesSent > 0 ||
70
+ report.derivedSent.length > 0 ||
71
+ report.pulled > 0;
57
72
  const defaultRequest = async (url, init) => {
58
73
  const res = await fetch(url, init);
59
74
  return { status: res.status, text: await res.text() };
@@ -141,11 +156,9 @@ export function startSyncDaemon(deps) {
141
156
  log('reticle_cloud_sync_failed', { root, error: report.error });
142
157
  continue;
143
158
  }
144
- const moved = report.runsSent > 0 ||
145
- report.flowsSent > 0 ||
146
- report.capsulesSent > 0 ||
147
- report.derivedSent.length > 0 ||
148
- report.pulled > 0;
159
+ if (!report.ok)
160
+ log('reticle_cloud_sync_failed', { root, error: describeSync(report) });
161
+ const moved = movedAnything(report);
149
162
  // The root is NAMED here and not in the single-root log below, because with several repos
150
163
  // reporting, "synced 3 runs" without a directory is not an answer to "synced from where".
151
164
  if (moved)
@@ -196,27 +209,27 @@ export function startSyncDaemon(deps) {
196
209
  // failed. The log below is deliberately quiet when idle, which is right for a log and wrong
197
210
  // for a hook: from outside, "nothing changed" and "sync has been broken since Tuesday" are
198
211
  // the same silence, and a consumer that cannot tell them apart has to guess.
212
+ /*
213
+ * `ok`, not "no transport error": a push the server refused completed its HTTP exchange and
214
+ * still left the dashboard without the thing. The refusal's reason rides as the error.
215
+ */
216
+ const failure = report.error ?? (report.ok ? undefined : describeSync(report));
199
217
  emitSyncHook({
200
218
  runsPushed: report.runsSent,
201
- ok: report.error === undefined,
202
- ...(report.error === undefined ? {} : { error: report.error }),
219
+ ok: report.ok,
220
+ ...(failure === undefined ? {} : { error: failure }),
203
221
  });
204
- if (report.error !== undefined) {
205
- if (report.error !== reportedError) {
206
- reportedError = report.error;
207
- log('reticle_cloud_sync_failed', { error: report.error });
208
- }
222
+ if (failure !== undefined && failure !== reportedError) {
223
+ reportedError = failure;
224
+ log('reticle_cloud_sync_failed', { error: failure });
209
225
  }
210
- else {
211
- reportedError = undefined;
226
+ if (report.error === undefined) {
227
+ if (failure === undefined)
228
+ reportedError = undefined;
212
229
  // Only when something actually moved. A per-minute "nothing to send" is noise that trains
213
230
  // people to stop reading the log.
214
- const moved = report.runsSent > 0 ||
215
- report.flowsSent > 0 ||
216
- report.capsulesSent > 0 ||
217
- report.derivedSent.length > 0 ||
218
- report.pulled > 0;
219
- if (moved)
231
+ const moved = movedAnything(report);
232
+ if (moved && failure === undefined)
220
233
  log('reticle_cloud_synced', { summary: describeSync(report) });
221
234
  /*
222
235
  * A cycle that moved something means a drive is in progress, so the next one comes sooner;
@@ -270,6 +283,18 @@ export function startSyncDaemon(deps) {
270
283
  clearTimeout(timer);
271
284
  timer = undefined;
272
285
  },
286
+ flush: async () => {
287
+ stopped = true;
288
+ if (timer !== undefined)
289
+ clearTimeout(timer);
290
+ timer = undefined;
291
+ // A cycle already running may have started BEFORE the write being flushed, so its report says
292
+ // nothing about that artifact. Wait it out rather than skipping on the overlap guard.
293
+ const deadline = Date.now() + FLUSH_WAIT_MS;
294
+ while (running && Date.now() < deadline)
295
+ await new Promise((resolve) => setTimeout(resolve, FLUSH_POLL_MS));
296
+ await cycle();
297
+ },
273
298
  };
274
299
  }
275
300
  /**
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync, } from 'node:fs';
14
14
  import { dirname, join } from 'node:path';
15
- import { ReticleDir } from '@reticlehq/core';
15
+ import { IntentDir, ReticleDir } from '@reticlehq/core';
16
16
  import { subjectFor } from '../intent/intent-subject.js';
17
17
  const JSON_SUFFIX = '.json';
18
18
  /** Read + parse, or undefined for absent/unreadable/malformed. Never throws. */
@@ -55,13 +55,25 @@ function readJsonDir(dir) {
55
55
  return [];
56
56
  }
57
57
  }
58
- /** Flows live one directory deeper, under the app's own project id. */
58
+ /**
59
+ * Flows: one directory deeper under the app's own project id, and also flat in `.reticle/flows/`,
60
+ * which the flow store still saves and loads (a flow saved with no project id, or from before flows
61
+ * were kept per project). Reading only the subfolders skipped every flat flow while the cycle
62
+ * reported ok.
63
+ */
59
64
  function readFlows(root) {
60
65
  const dir = join(root, ReticleDir.FLOWS_SUBDIR);
61
66
  try {
62
67
  if (!existsSync(dir))
63
68
  return [];
64
- return readdirSync(dir).flatMap((scope) => readJsonDir(join(dir, scope)));
69
+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
70
+ if (entry.isDirectory())
71
+ return readJsonDir(join(dir, entry.name));
72
+ if (!entry.name.endsWith(JSON_SUFFIX))
73
+ return [];
74
+ const flow = readJson(join(dir, entry.name));
75
+ return flow === undefined ? [] : [flow];
76
+ });
65
77
  }
66
78
  catch {
67
79
  return [];
@@ -71,10 +83,12 @@ const DERIVED_FILE = {
71
83
  impact: ReticleDir.IMPACT_FILE,
72
84
  flake: ReticleDir.FLAKE_FILE,
73
85
  intent: ReticleDir.INTENT_FILE,
86
+ envelopes: ReticleDir.ENVELOPES_FILE,
87
+ 'assertion-tiers': ReticleDir.TIERS_FILE,
74
88
  };
75
89
  /** The directory the sharded intent store writes into, beside the legacy flat file. */
76
- const INTENT_SUBDIR = 'intent';
77
- const INTENT_INDEX = 'index.json';
90
+ const INTENT_SUBDIR = ReticleDir.INTENT_SUBDIR;
91
+ const INTENT_INDEX = IntentDir.INDEX_FILE;
78
92
  /**
79
93
  * Every intent this project holds, flat file and shards merged.
80
94
  *
@@ -98,10 +112,15 @@ function readIntent(reticleRoot) {
98
112
  ...(legacy?.intents ?? {}),
99
113
  };
100
114
  let shards = 0;
101
- for (const file of safeReaddir(join(reticleRoot, INTENT_SUBDIR))) {
102
- if (!file.endsWith(JSON_SUFFIX) || INTENT_INDEX === file)
115
+ for (const entry of safeReaddir(join(reticleRoot, INTENT_SUBDIR))) {
116
+ if (INTENT_INDEX === entry)
103
117
  continue;
104
- const shard = readJson(join(reticleRoot, INTENT_SUBDIR, file));
118
+ // `<subject>/intent.json` is the layout; a bare `<subject>.json` is an interim shard the store
119
+ // has not migrated yet, and is still this project's memory until it has.
120
+ const path = entry.endsWith(JSON_SUFFIX)
121
+ ? join(reticleRoot, INTENT_SUBDIR, entry)
122
+ : join(reticleRoot, INTENT_SUBDIR, entry, IntentDir.SHARD_FILE);
123
+ const shard = readJson(path);
105
124
  // A hand-edited or half-written shard costs one subject, never the whole sync.
106
125
  if (shard?.intents === undefined)
107
126
  continue;