@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
@@ -221,7 +221,13 @@ export class ImpactStore {
221
221
  if (this.#dashboardUrl !== undefined)
222
222
  snap.dashboardUrl = this.#dashboardUrl;
223
223
  snap.account = this.#account();
224
+ const cfgForOffer = this.#config.read();
224
225
  const offer = this.#offer.read();
226
+ // Joined here because this is the one place that holds BOTH the offer and the platform config.
227
+ // The card reads the offer, so readiness has to travel on it rather than being re-derived in a
228
+ // presenter that has no business knowing what a provider key is.
229
+ if (offer !== undefined && cfgForOffer !== undefined)
230
+ offer.drivable = cfgForOffer.providerReady;
225
231
  // Absent means "we have not heard", which the HUD renders as nothing at all. Never defaulted to
226
232
  // `{claimed:false}`: that would advertise the offer to everyone who is offline.
227
233
  if (offer !== undefined)
@@ -9,7 +9,7 @@ import { sessionRoot } from '../project/session-root.js';
9
9
  * and it does. `reticle_act_and_wait` and `reticle_assert` are in every agent's tool list already,
10
10
  * so one optional argument there makes the declaration free and discoverable by construction.
11
11
  *
12
- * This is a shortcut into the EXISTING ledger, never a second one. It writes `.reticle/intent.json`
12
+ * This is a shortcut into the EXISTING ledger, never a second one. It writes `.reticle/intent/`
13
13
  * through `IntentStore` exactly as `reticle_intent { action: "declare" }` does, so a reviewer reads
14
14
  * one file in one vocabulary and cannot tell from the row which door the intent came in by. The
15
15
  * pattern is `flows/flow-intent.ts`, which does the same job for a saved flow.
@@ -146,8 +146,13 @@ binding) {
146
146
  // Declared WITHOUT a surface on purpose: see dischargeInlineIntent for why the route that
147
147
  // describes this record only exists after the action it is about.
148
148
  const declaredHere = existing === undefined;
149
- if (declaredHere)
150
- await store.declare([{ id, statement: intent }]);
149
+ if (declaredHere) {
150
+ // Nothing stored (a step label is refused) means nothing to link: no verdict may then claim
151
+ // to have proved an intent that was never written.
152
+ const stored = await store.declare([{ id, statement: intent }]);
153
+ if (0 === stored.length)
154
+ return undefined;
155
+ }
151
156
  /*
152
157
  * A binding may be INVENTED only for a row this call also declared.
153
158
  *
@@ -1,29 +1,31 @@
1
1
  /**
2
- * Reading and writing the sharded intent store, and migrating the flat one into it.
2
+ * The intent ledger on disk: one directory per subject, and a flow's name IS its subject.
3
3
  *
4
- * The operations an agent actually performs, and what each is allowed to cost:
4
+ * .reticle/intent/index.json every id, its subject and one line — derived
5
+ * .reticle/intent/<subject>/intent.json the full records for that subject
5
6
  *
6
- * index() every id + one line, always affordable — the session-start read
7
- * subject(name) the full records for one subject, when it is about to work on that
8
- * get(id) one record, via the index rather than by scanning shards
9
- * record()/update() a write that touches ONE shard file plus the index
7
+ * The ONE place intents are stored. `IntentStore` is the domain API over it (declare, bind,
8
+ * discharge) and never writes a file of its own; `reticle_intent`'s record/get/index/subject read
9
+ * this directly. Two stores kept in two files is how an intent written through one action became
10
+ * invisible to the other, and an agent told its own intent does not exist concludes it does not.
10
11
  *
11
- * The last one is the reason for the whole design. Under the flat file, changing one intent rewrote
12
- * all 141, so two sessions touching unrelated subjects raced each other and the loser's write
13
- * vanished. Sharded, they only collide when they genuinely edit the same subject.
12
+ * Git-checked on purpose: a human sees in review when an intent is narrowed to match what was easy
13
+ * to prove. Writes are byte-stable, and a write that changes nothing writes nothing, because a ledger
14
+ * that churns on every run is one whose diffs nobody reads.
14
15
  *
15
- * ## Migration is automatic, and it does not delete
16
+ * ## Migration happens on the first write, and it removes what it moved
16
17
  *
17
- * `.reticle/intent.json` is read on every load and folded in when the sharded copy has nothing for
18
- * an id. That makes the move invisible: an agent on the new store sees old intents immediately, and
19
- * an older build still reading the flat file keeps working, because nothing removes it. Deleting it
20
- * is a separate decision somebody can take once they believe the migration.
18
+ * The old single `.reticle/intent.json` (and any interim `intent/<subject>.json`) is read until then,
19
+ * so nothing is invisible before the move. The first write folds every record into its directory and
20
+ * deletes the old files, so the change shows up in one reviewable diff. A legacy file that does not
21
+ * PARSE is never deleted: a hand-merge leaves conflict markers, it reads as empty, and removing it
22
+ * would destroy the only copy of every intent inside.
21
23
  */
22
24
  import { type Intent } from '@reticlehq/core/artifacts';
23
25
  import type { FileSystemPort } from '../project/fs/fs-port.js';
24
26
  import type { Clock } from '../../machine/clock.js';
25
27
  import { IntentStatus, type IntentIndex, type IntentRecord } from './intent-shard.js';
26
- /** What a caller supplies to write an intent. Everything optional is genuinely optional. */
28
+ /** What a caller supplies to write an intent through `record`. Everything optional is optional. */
27
29
  interface IntentInput {
28
30
  id: string;
29
31
  statement: string;
@@ -34,51 +36,35 @@ interface IntentInput {
34
36
  surface?: Intent['surface'];
35
37
  binding?: unknown;
36
38
  }
39
+ /** The ledger-level facts a write may set beside the intent itself. */
40
+ export interface RecordFacts {
41
+ subject?: string | undefined;
42
+ status?: IntentStatus | undefined;
43
+ why?: string | undefined;
44
+ source?: string | undefined;
45
+ }
37
46
  export declare class IntentShardStore {
38
47
  #private;
39
48
  constructor(fs: FileSystemPort, root: string, clock: Clock);
40
- /**
41
- * Every record: the shards, plus any legacy intent the shards have not absorbed.
42
- *
43
- * Sharded wins on a collision. An id present in both has been migrated and possibly edited since,
44
- * and letting the flat copy overwrite that would silently revert the edit.
45
- */
46
- /**
47
- * Every record this ledger holds, from BOTH files, deduped by id.
48
- *
49
- * Public because the ledger is one logical thing kept in two places, and the tool that offers
50
- * `list` has to be able to see all of it. It was private while `record` wrote to shards and `list`
51
- * read the flat file, which meant an intent written through one action was invisible to the other
52
- * — and an agent told its own recorded intent does not exist concludes the intent does not exist,
53
- * not that there are two stores.
54
- */
49
+ /** Every record: the directories, plus any legacy intent not yet moved. Directories win. */
55
50
  all(): Promise<IntentRecord[]>;
56
- /**
57
- * The cheap read: one line per intent, and the call an agent makes at the start of a session.
58
- *
59
- * Includes anything still only in the legacy flat file, so the very first call on an unmigrated
60
- * project already answers completely — which is what makes the migration invisible rather than a
61
- * step somebody has to remember to run.
62
- */
51
+ /** The cheap read: one line per intent. Derived from disk, never from the stored copy. */
63
52
  index(): Promise<IntentIndex>;
64
53
  /** Every record for one subject — the working read, once an agent knows what it is touching. */
65
54
  subject(name: string): Promise<IntentRecord[]>;
66
55
  /** One record by id, or null. */
67
56
  get(id: string): Promise<IntentRecord | null>;
68
57
  /**
69
- * Write one intent, creating or updating it.
58
+ * Create or change ONE intent atomically: read, transform, write, under the ledger's lock.
70
59
  *
71
- * Merges onto whatever is already stored, so a caller adding a `why` to a migrated record does not
72
- * have to restate its binding — and cannot silently drop it by omission, which is the failure mode
73
- * that makes agents afraid to touch a store.
60
+ * `transform` returns the next intent, or undefined for "leave it as it is". A result equal to
61
+ * what is stored writes nothing. Returns the stored record, or undefined when nothing was there
62
+ * and nothing was created.
74
63
  */
64
+ upsert(id: string, transform: (existing: IntentRecord | undefined) => Intent | undefined, facts?: RecordFacts): Promise<IntentRecord | undefined>;
65
+ /** Write one intent through `reticle_intent { action: "record" }`, merging onto what is stored. */
75
66
  record(input: IntentInput): Promise<IntentRecord>;
76
- /**
77
- * Fold the flat file into shards on disk, so the move is visible in a diff rather than implicit.
78
- *
79
- * Idempotent, and it does not delete `.reticle/intent.json`: an older build still reads it, and a
80
- * migration that removes its own source cannot be checked afterwards.
81
- */
67
+ /** `reticle_intent { action: "migrate" }`: do now what the next write would do anyway. */
82
68
  migrate(): Promise<{
83
69
  migrated: number;
84
70
  subjects: string[];
@@ -1,32 +1,38 @@
1
1
  /**
2
- * Reading and writing the sharded intent store, and migrating the flat one into it.
2
+ * The intent ledger on disk: one directory per subject, and a flow's name IS its subject.
3
3
  *
4
- * The operations an agent actually performs, and what each is allowed to cost:
4
+ * .reticle/intent/index.json every id, its subject and one line — derived
5
+ * .reticle/intent/<subject>/intent.json the full records for that subject
5
6
  *
6
- * index() every id + one line, always affordable — the session-start read
7
- * subject(name) the full records for one subject, when it is about to work on that
8
- * get(id) one record, via the index rather than by scanning shards
9
- * record()/update() a write that touches ONE shard file plus the index
7
+ * The ONE place intents are stored. `IntentStore` is the domain API over it (declare, bind,
8
+ * discharge) and never writes a file of its own; `reticle_intent`'s record/get/index/subject read
9
+ * this directly. Two stores kept in two files is how an intent written through one action became
10
+ * invisible to the other, and an agent told its own intent does not exist concludes it does not.
10
11
  *
11
- * The last one is the reason for the whole design. Under the flat file, changing one intent rewrote
12
- * all 141, so two sessions touching unrelated subjects raced each other and the loser's write
13
- * vanished. Sharded, they only collide when they genuinely edit the same subject.
12
+ * Git-checked on purpose: a human sees in review when an intent is narrowed to match what was easy
13
+ * to prove. Writes are byte-stable, and a write that changes nothing writes nothing, because a ledger
14
+ * that churns on every run is one whose diffs nobody reads.
14
15
  *
15
- * ## Migration is automatic, and it does not delete
16
+ * ## Migration happens on the first write, and it removes what it moved
16
17
  *
17
- * `.reticle/intent.json` is read on every load and folded in when the sharded copy has nothing for
18
- * an id. That makes the move invisible: an agent on the new store sees old intents immediately, and
19
- * an older build still reading the flat file keeps working, because nothing removes it. Deleting it
20
- * is a separate decision somebody can take once they believe the migration.
18
+ * The old single `.reticle/intent.json` (and any interim `intent/<subject>.json`) is read until then,
19
+ * so nothing is invisible before the move. The first write folds every record into its directory and
20
+ * deletes the old files, so the change shows up in one reviewable diff. A legacy file that does not
21
+ * PARSE is never deleted: a hand-merge leaves conflict markers, it reads as empty, and removing it
22
+ * would destroy the only copy of every intent inside.
21
23
  */
22
- import { parseIntentFile } from '@reticlehq/core/artifacts';
23
- import { ReticleDir } from '@reticlehq/core';
24
+ import { IntentFileSchema, IntentState, amendIntent, isActionLabel, } from '@reticlehq/core/artifacts';
25
+ import { IntentDir, ReticleDir } from '@reticlehq/core';
24
26
  import { reticleDirPaths } from '../project/dir/reticle-dir.js';
25
27
  import { withFileLock } from '../project/file-lock.js';
26
28
  import { subjectFor } from './intent-subject.js';
27
- import { emptyShard, indexFrom, IntentShardSchema, IntentStatus, recordFromIntent, serialise, shardsFrom, readJsonFile, } from './intent-shard.js';
28
- const INDEX_FILE = 'index.json';
29
- const SHARD_SUFFIX = '.json';
29
+ import { emptyShard, indexFrom, IntentShardSchema, IntentStatus, recordFromIntent, serialise, shardsFrom, statusFromState, } from './intent-shard.js';
30
+ import { writeFileAtomic } from '../project/fs/write-atomic.js';
31
+ const JSON_SUFFIX = '.json';
32
+ /** Beside an unparseable shard: its bytes, kept before it is rewritten. See `#shardForWrite`. */
33
+ const UNREADABLE_SUFFIX = '.unreadable-';
34
+ /** The intent fields a record is compared on — `updatedAt` alone changing is not a change. */
35
+ const comparable = (record) => serialise({ ...record, updatedAt: 0 });
30
36
  export class IntentShardStore {
31
37
  #fs;
32
38
  #root;
@@ -40,175 +46,281 @@ export class IntentShardStore {
40
46
  return `${reticleDirPaths(this.#root).root}/${ReticleDir.INTENT_SUBDIR}`;
41
47
  }
42
48
  #indexPath() {
43
- return `${this.#dir()}/${INDEX_FILE}`;
49
+ return `${this.#dir()}/${IntentDir.INDEX_FILE}`;
44
50
  }
45
51
  #shardPath(subject) {
46
- return `${this.#dir()}/${subject}${SHARD_SUFFIX}`;
52
+ return `${this.#dir()}/${subject}/${IntentDir.SHARD_FILE}`;
47
53
  }
48
- /** Legacy records, keyed by id. Empty when there is no flat file, which is the eventual case. */
49
- async #legacy() {
50
- const file = await readJsonFile(this.#fs, reticleDirPaths(this.#root).intent, (raw) => parseIntentFile(raw), { version: 1, intents: {} });
51
- return new Map(Object.values(file.intents).map((i) => [i.id, recordFromIntent(i)]));
54
+ /** Parse one legacy file, remembering whether it parsed — only a parsed file may be deleted. */
55
+ async #readLegacy(pathOf, toRecords) {
56
+ let path = '';
57
+ let text;
58
+ try {
59
+ // Resolved INSIDE the try: with no known project root there is no path, and that must read as
60
+ // "no legacy file" — the old store failed soft here, and a caller only asking what is still
61
+ // open must not be taken down by it.
62
+ path = pathOf();
63
+ text = await this.#fs.readFile(path);
64
+ }
65
+ catch {
66
+ return { path, records: [], parsed: true };
67
+ }
68
+ try {
69
+ return { path, records: toRecords(JSON.parse(text)), parsed: true };
70
+ }
71
+ catch {
72
+ return { path, records: [], parsed: false };
73
+ }
52
74
  }
53
- async #readShard(subject) {
54
- return readJsonFile(this.#fs, this.#shardPath(subject), (raw) => IntentShardSchema.parse(raw), emptyShard(subject));
75
+ /** The old single file, and any interim `intent/<subject>.json` from before directories. */
76
+ async #legacySources() {
77
+ const flat = await this.#readLegacy(() => reticleDirPaths(this.#root).intent, (raw) => Object.values(IntentFileSchema.parse(raw).intents).map(recordFromIntent));
78
+ const interim = await Promise.all((await this.#entries())
79
+ .filter((e) => e.endsWith(JSON_SUFFIX) && IntentDir.INDEX_FILE !== e)
80
+ .map((e) => this.#readLegacy(() => `${this.#dir()}/${e}`, (raw) => Object.values(IntentShardSchema.parse(raw).intents))));
81
+ return [flat, ...interim].filter((l) => l.records.length > 0 || !l.parsed);
82
+ }
83
+ async #entries() {
84
+ try {
85
+ return await this.#fs.readdir(this.#dir());
86
+ }
87
+ catch {
88
+ return [];
89
+ }
55
90
  }
56
91
  /**
57
92
  * Which subjects exist, read from the DIRECTORY rather than the index.
58
93
  *
59
- * The index cannot be the source here. It is derived from the shards, so discovering shards
60
- * through it is circular — and the circle is not theoretical: a brand-new subject was invisible to
61
- * the very write that created it, because its shard existed on disk while the index did not
62
- * mention it yet. Listing the directory also makes the index self-healing: delete it and the next
63
- * read rebuilds it from what is actually there.
94
+ * The index is derived from the shards, so discovering shards through it would be circular: a new
95
+ * subject was once invisible to the very write that created it. Listing the directory also makes
96
+ * the index self-healing — delete it and the next write rebuilds it from what is actually there.
64
97
  */
65
98
  async #subjectsOnDisk() {
99
+ const subjects = [];
100
+ for (const entry of await this.#entries()) {
101
+ if (entry.endsWith(JSON_SUFFIX))
102
+ continue;
103
+ if (await this.#fs.exists(this.#shardPath(entry)))
104
+ subjects.push(entry);
105
+ }
106
+ return subjects;
107
+ }
108
+ async #readShard(subject) {
66
109
  try {
67
- const entries = await this.#fs.readdir(this.#dir());
68
- return entries
69
- .filter((e) => e.endsWith(SHARD_SUFFIX) && INDEX_FILE !== e)
70
- .map((e) => e.slice(0, -SHARD_SUFFIX.length));
110
+ return IntentShardSchema.parse(JSON.parse(await this.#fs.readFile(this.#shardPath(subject))));
71
111
  }
72
112
  catch {
73
- // No directory yet — an unmigrated project, which the legacy fold below still answers for.
74
- return [];
113
+ return emptyShard(subject);
75
114
  }
76
115
  }
77
116
  /**
78
- * Every record: the shards, plus any legacy intent the shards have not absorbed.
117
+ * A shard about to be rewritten, read so that nothing in it can be lost.
79
118
  *
80
- * Sharded wins on a collision. An id present in both has been migrated and possibly edited since,
81
- * and letting the flat copy overwrite that would silently revert the edit.
119
+ * `#readShard` answers "what is recorded here" and treats an unparseable shard as empty, which is
120
+ * right for reading and wrong for rewriting: the next write put "empty plus one record" over a file
121
+ * that held every other intent for that subject (#994). An unreadable shard's bytes are copied
122
+ * beside it, untouched, before it is replaced, the same rule migration keeps for a legacy file it
123
+ * cannot parse. A missing shard is simply empty.
82
124
  */
83
- /**
84
- * Every record this ledger holds, from BOTH files, deduped by id.
85
- *
86
- * Public because the ledger is one logical thing kept in two places, and the tool that offers
87
- * `list` has to be able to see all of it. It was private while `record` wrote to shards and `list`
88
- * read the flat file, which meant an intent written through one action was invisible to the other
89
- * — and an agent told its own recorded intent does not exist concludes the intent does not exist,
90
- * not that there are two stores.
91
- */
92
- async all() {
93
- return this.#all();
125
+ async #shardForWrite(subject) {
126
+ const path = this.#shardPath(subject);
127
+ let text;
128
+ try {
129
+ text = await this.#fs.readFile(path);
130
+ }
131
+ catch {
132
+ return emptyShard(subject);
133
+ }
134
+ try {
135
+ return IntentShardSchema.parse(JSON.parse(text));
136
+ }
137
+ catch {
138
+ await writeFileAtomic(this.#fs, `${path}${UNREADABLE_SUFFIX}${String(this.#clock.now())}`, text);
139
+ return emptyShard(subject);
140
+ }
94
141
  }
95
- async #all() {
142
+ async #storedRecords() {
96
143
  const shards = await Promise.all((await this.#subjectsOnDisk()).map((s) => this.#readShard(s)));
144
+ return shards.flatMap((s) => Object.values(s.intents));
145
+ }
146
+ /** Every record: the directories, plus any legacy intent not yet moved. Directories win. */
147
+ async all() {
97
148
  const merged = new Map();
98
- for (const [id, record] of await this.#legacy())
99
- merged.set(id, record);
100
- for (const shard of shards) {
101
- for (const record of Object.values(shard.intents))
149
+ for (const legacy of await this.#legacySources()) {
150
+ for (const record of legacy.records)
102
151
  merged.set(record.id, record);
103
152
  }
104
- return [...merged.values()];
153
+ for (const record of await this.#storedRecords())
154
+ merged.set(record.id, record);
155
+ return [...merged.values()].sort((a, b) => a.declaredAt - b.declaredAt || a.id.localeCompare(b.id));
105
156
  }
106
- /**
107
- * The cheap read: one line per intent, and the call an agent makes at the start of a session.
108
- *
109
- * Includes anything still only in the legacy flat file, so the very first call on an unmigrated
110
- * project already answers completely — which is what makes the migration invisible rather than a
111
- * step somebody has to remember to run.
112
- */
157
+ /** The cheap read: one line per intent. Derived from disk, never from the stored copy. */
113
158
  async index() {
114
- // Derived from what is on disk, never from the stored copy. The stored index is a cache for
115
- // readers outside this process; inside it, trusting it would let a stale one hide a real record.
116
- return indexFrom(shardsFrom(await this.#all()));
117
- }
118
- /** Records already in shards, ignoring the legacy file — what migration diffs against. */
119
- async #storedRecords() {
120
- const shards = await Promise.all((await this.#subjectsOnDisk()).map((s) => this.#readShard(s)));
121
- return shards.flatMap((s) => Object.values(s.intents));
159
+ return indexFrom(shardsFrom(await this.all()));
122
160
  }
123
161
  /** Every record for one subject — the working read, once an agent knows what it is touching. */
124
162
  async subject(name) {
125
- const all = await this.#all();
126
- return all.filter((r) => name === r.subject);
163
+ return (await this.all()).filter((r) => name === r.subject);
127
164
  }
128
165
  /** One record by id, or null. */
129
166
  async get(id) {
130
- const all = await this.#all();
131
- return all.find((r) => id === r.id) ?? null;
167
+ return (await this.all()).find((r) => id === r.id) ?? null;
132
168
  }
133
169
  /**
134
- * Write one intent, creating or updating it.
170
+ * Create or change ONE intent atomically: read, transform, write, under the ledger's lock.
135
171
  *
136
- * Merges onto whatever is already stored, so a caller adding a `why` to a migrated record does not
137
- * have to restate its binding — and cannot silently drop it by omission, which is the failure mode
138
- * that makes agents afraid to touch a store.
172
+ * `transform` returns the next intent, or undefined for "leave it as it is". A result equal to
173
+ * what is stored writes nothing. Returns the stored record, or undefined when nothing was there
174
+ * and nothing was created.
139
175
  */
140
- async record(input) {
176
+ async upsert(id, transform, facts = {}) {
141
177
  return withFileLock(this.#indexPath(), async () => {
142
- const existing = await this.get(input.id);
143
- const now = this.#clock.now();
144
- const subject = input.subject ??
145
- existing?.subject ??
146
- subjectFor({ surface: input.surface, binding: input.binding });
147
- const next = {
148
- ...(existing ?? {
149
- id: input.id,
150
- statement: input.statement,
151
- state: 'declared',
152
- declaredAt: now,
153
- subject,
154
- status: IntentStatus.PROPOSED,
155
- }),
156
- statement: input.statement,
157
- subject,
158
- status: input.status ?? existing?.status ?? IntentStatus.PROPOSED,
159
- updatedAt: now,
160
- ...(input.why === undefined ? {} : { why: input.why }),
161
- ...(input.source === undefined ? {} : { source: input.source }),
162
- ...(input.surface === undefined ? {} : { surface: input.surface }),
163
- ...(input.binding === undefined ? {} : { binding: input.binding }),
164
- };
165
- await this.#write(next, existing?.subject);
166
- return next;
178
+ await this.#migrateLegacy();
179
+ await this.#retireLabels();
180
+ const existing = (await this.#storedRecords()).find((r) => id === r.id);
181
+ const next = transform(existing);
182
+ if (next === undefined)
183
+ return existing;
184
+ const record = this.#recordFrom(amendIntent(existing, next), existing, facts);
185
+ if (existing !== undefined && comparable(existing) === comparable(record))
186
+ return existing;
187
+ await this.#write(record, existing?.subject);
188
+ return record;
167
189
  });
168
190
  }
169
- /** Persist one record, rewriting its shard and the index — and the OLD shard if it moved. */
191
+ /**
192
+ * The ledger-level fields of a record: where it lives and how settled it is.
193
+ *
194
+ * A flow's name IS its subject, so an intent a flow claims moves into that flow's directory.
195
+ * Otherwise an explicit subject wins, then the one it already had, then inference.
196
+ */
197
+ #recordFrom(next, existing, facts) {
198
+ const flow = next.surface?.flow;
199
+ const subject = flow !== undefined
200
+ ? subjectFor({ surface: { flow } })
201
+ : (facts.subject ??
202
+ existing?.subject ??
203
+ subjectFor({ surface: next.surface, binding: next.binding }));
204
+ const why = facts.why ?? existing?.why;
205
+ const source = facts.source ?? existing?.source;
206
+ return {
207
+ ...existing,
208
+ ...next,
209
+ subject,
210
+ status: this.#statusOf(next, existing, facts.status),
211
+ updatedAt: this.#clock.now(),
212
+ ...(why === undefined ? {} : { why }),
213
+ ...(source === undefined ? {} : { source }),
214
+ };
215
+ }
216
+ /**
217
+ * How settled the record is. A verdict proving it makes it proved; losing that proof (the words
218
+ * changed, a different check was bound) steps it back rather than leaving a stale "proved".
219
+ */
220
+ #statusOf(next, existing, asked) {
221
+ if (asked !== undefined)
222
+ return asked;
223
+ if (IntentState.PROVED === next.state)
224
+ return IntentStatus.PROVED;
225
+ if (IntentStatus.PROVED === existing?.status)
226
+ return statusFromState(next.state);
227
+ return existing?.status ?? IntentStatus.PROPOSED;
228
+ }
229
+ /** Write one intent through `reticle_intent { action: "record" }`, merging onto what is stored. */
230
+ async record(input) {
231
+ const now = this.#clock.now();
232
+ const stored = await this.upsert(input.id, (existing) => ({
233
+ ...(existing ?? { id: input.id, state: IntentState.DECLARED, declaredAt: now }),
234
+ statement: input.statement,
235
+ ...(input.surface === undefined ? {} : { surface: input.surface }),
236
+ ...(input.binding === undefined ? {} : { binding: input.binding }),
237
+ }), { subject: input.subject, status: input.status, why: input.why, source: input.source });
238
+ // upsert only returns undefined when the transform declines, and this one never does.
239
+ return stored;
240
+ }
241
+ /** Persist one record, rewriting its shard and the index, and removing it from its old shard. */
170
242
  async #write(record, previousSubject) {
171
- await this.#fs.mkdir(this.#dir());
172
243
  if (previousSubject !== undefined && record.subject !== previousSubject) {
173
- const old = await this.#readShard(previousSubject);
174
- if (old.intents[record.id] !== undefined) {
175
- const { [record.id]: _moved, ...rest } = old.intents;
176
- await this.#fs.writeFile(this.#shardPath(previousSubject), serialise({ ...old, intents: rest }));
177
- }
244
+ const old = await this.#shardForWrite(previousSubject);
245
+ const { [record.id]: _moved, ...rest } = old.intents;
246
+ await this.#putShard({ ...old, intents: rest });
178
247
  }
179
- const shard = await this.#readShard(record.subject);
248
+ const shard = await this.#shardForWrite(record.subject);
180
249
  shard.intents[record.id] = record;
181
- await this.#fs.writeFile(this.#shardPath(record.subject), serialise(shard));
182
- // The index is DERIVED, never edited in place: two sources of truth would disagree, and the one
183
- // that is cheap to read is the one people would trust.
184
- const all = await this.#all();
185
- await this.#fs.writeFile(this.#indexPath(), serialise(indexFrom(shardsFrom(all))));
250
+ await this.#putShard(shard);
251
+ await this.#writeIndex();
252
+ }
253
+ /** Write a shard, or remove it when it no longer holds anything. */
254
+ async #putShard(shard) {
255
+ const path = this.#shardPath(shard.subject);
256
+ if (0 === Object.keys(shard.intents).length) {
257
+ await this.#fs.rm(path).catch(() => undefined);
258
+ return;
259
+ }
260
+ // Checked against the schema the reader applies, BEFORE it reaches the disk: a record the
261
+ // reader would refuse makes the whole shard unreadable, and every other intent in it with it.
262
+ // Refused here, loudly, where the one bad write is still the caller's to fix (#1020).
263
+ const valid = IntentShardSchema.parse(shard);
264
+ await this.#fs.mkdir(`${this.#dir()}/${shard.subject}`);
265
+ await writeFileAtomic(this.#fs, path, serialise(valid));
266
+ }
267
+ async #writeIndex() {
268
+ await this.#fs.mkdir(this.#dir());
269
+ const index = indexFrom(shardsFrom(await this.#storedRecords()));
270
+ await writeFileAtomic(this.#fs, this.#indexPath(), serialise(index));
186
271
  }
187
272
  /**
188
- * Fold the flat file into shards on disk, so the move is visible in a diff rather than implicit.
189
- *
190
- * Idempotent, and it does not delete `.reticle/intent.json`: an older build still reads it, and a
191
- * migration that removes its own source cannot be checked afterwards.
273
+ * Move every legacy record into its directory, then remove the legacy files that parsed.
274
+ * Called under the lock by every write, so the first write after an upgrade is the migration.
192
275
  */
276
+ async #migrateLegacy() {
277
+ const sources = await this.#legacySources();
278
+ if (0 === sources.length)
279
+ return { migrated: [] };
280
+ const known = new Set((await this.#storedRecords()).map((r) => r.id));
281
+ const incoming = sources.flatMap((s) => s.records).filter((r) => !known.has(r.id));
282
+ for (const shard of shardsFrom(incoming)) {
283
+ const current = await this.#shardForWrite(shard.subject);
284
+ await this.#putShard({ ...current, intents: { ...shard.intents, ...current.intents } });
285
+ }
286
+ for (const source of sources) {
287
+ if (source.parsed)
288
+ await this.#fs.rm(source.path).catch(() => undefined);
289
+ }
290
+ await this.#writeIndex();
291
+ return { migrated: incoming };
292
+ }
293
+ /**
294
+ * Mark every stored step label stale: "click button \"Cancel\"" describes what was DONE, not a
295
+ * rule. Kept rather than deleted, because removing a record loses the fact it was ever written.
296
+ * Runs with every write, and writes nothing once there is nothing left to retire.
297
+ */
298
+ async #retireLabels() {
299
+ const labels = (await this.#storedRecords()).filter((r) => IntentStatus.STALE !== r.status && isActionLabel(r.statement));
300
+ if (0 === labels.length)
301
+ return 0;
302
+ for (const subject of new Set(labels.map((r) => r.subject))) {
303
+ const shard = await this.#shardForWrite(subject);
304
+ for (const record of labels.filter((r) => subject === r.subject)) {
305
+ shard.intents[record.id] = {
306
+ ...record,
307
+ status: IntentStatus.STALE,
308
+ updatedAt: this.#clock.now(),
309
+ };
310
+ }
311
+ await this.#putShard(shard);
312
+ }
313
+ await this.#writeIndex();
314
+ return labels.length;
315
+ }
316
+ /** `reticle_intent { action: "migrate" }`: do now what the next write would do anyway. */
193
317
  async migrate() {
194
318
  return withFileLock(this.#indexPath(), async () => {
195
- const legacy = await this.#legacy();
196
- if (0 === legacy.size)
197
- return { migrated: 0, subjects: [] };
198
- const stored = await this.#storedRecords();
199
- const known = new Set(stored.map((r) => r.id));
200
- const incoming = [...legacy.values()].filter((r) => !known.has(r.id));
201
- if (0 === incoming.length)
202
- return { migrated: 0, subjects: [] };
203
- const shards = shardsFrom([...stored, ...incoming]);
204
- await this.#fs.mkdir(this.#dir());
205
- for (const shard of shards) {
206
- await this.#fs.writeFile(this.#shardPath(shard.subject), serialise(shard));
207
- }
208
- await this.#fs.writeFile(this.#indexPath(), serialise(indexFrom(shards)));
319
+ const { migrated } = await this.#migrateLegacy();
320
+ await this.#retireLabels();
209
321
  return {
210
- migrated: incoming.length,
211
- subjects: [...new Set(incoming.map((r) => r.subject))].sort(),
322
+ migrated: migrated.length,
323
+ subjects: [...new Set(migrated.map((r) => r.subject))].sort(),
212
324
  };
213
325
  });
214
326
  }