@reticlehq/server 3.1.0 → 3.3.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 (384) hide show
  1. package/SKILL.md +30 -28
  2. package/dist/command/cli/adhoc-suite.d.ts +19 -0
  3. package/dist/command/cli/adhoc-suite.js +88 -0
  4. package/dist/command/cli/adhoc-verdict.d.ts +23 -2
  5. package/dist/command/cli/adhoc-verdict.js +18 -10
  6. package/dist/command/cli/auth/device-flow.d.ts +2 -2
  7. package/dist/command/cli/cli-doctor.js +37 -5
  8. package/dist/command/cli/cli-flow-commands.d.ts +2 -1
  9. package/dist/command/cli/cli-kill.js +1 -1
  10. package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
  11. package/dist/command/cli/cli-parse-grammar.js +7 -0
  12. package/dist/command/cli/cli-parse-verify.d.ts +2 -5
  13. package/dist/command/cli/cli-parse-verify.js +86 -13
  14. package/dist/command/cli/cli-parse.d.ts +8 -1
  15. package/dist/command/cli/cli-parse.js +38 -7
  16. package/dist/command/cli/cli-verify.d.ts +59 -0
  17. package/dist/command/cli/cli-verify.js +131 -19
  18. package/dist/command/cli/cloud-cli.js +7 -6
  19. package/dist/command/cli/cloud-kit.d.ts +0 -2
  20. package/dist/command/cli/cloud-kit.js +3 -7
  21. package/dist/command/cli/config/config-discovery.d.ts +12 -0
  22. package/dist/command/cli/config/config-discovery.js +19 -0
  23. package/dist/command/cli/doctor/doctor-rows.d.ts +12 -0
  24. package/dist/command/cli/doctor/doctor-rows.js +12 -0
  25. package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
  26. package/dist/command/cli/drive/drive-command.js +1 -1
  27. package/dist/command/cli/launch/cli-launch.d.ts +3 -23
  28. package/dist/command/cli/launch/cli-launch.js +29 -70
  29. package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +33 -0
  30. package/dist/command/cli/lifecycle/daemon-lifecycle.js +198 -0
  31. package/dist/command/cli/mcp-command.js +5 -3
  32. package/dist/command/cli/ports/resolve/cli-port.d.ts +15 -1
  33. package/dist/command/cli/ports/resolve/cli-port.js +50 -2
  34. package/dist/command/cli/report-command.d.ts +16 -0
  35. package/dist/command/cli/report-command.js +97 -0
  36. package/dist/command/cli/status/status-lines.d.ts +28 -0
  37. package/dist/command/cli/status/status-lines.js +117 -0
  38. package/dist/command/cli.d.ts +1 -1
  39. package/dist/command/cli.js +29 -177
  40. package/dist/command/daemon/binding/daemon-status-probe.d.ts +32 -0
  41. package/dist/command/daemon/binding/daemon-status-probe.js +93 -0
  42. package/dist/command/daemon/binding/loopback-reach.d.ts +42 -0
  43. package/dist/command/daemon/binding/loopback-reach.js +63 -0
  44. package/dist/command/daemon/binding/port-presence.d.ts +1 -1
  45. package/dist/command/daemon/daemon-resilience.d.ts +13 -1
  46. package/dist/command/daemon/daemon-resilience.js +12 -1
  47. package/dist/command/daemon/daemon.d.ts +2 -11
  48. package/dist/command/daemon/daemon.js +8 -11
  49. package/dist/command/daemon/idle-shutdown.d.ts +2 -1
  50. package/dist/command/daemon/lifetime/heartbeat.d.ts +8 -0
  51. package/dist/command/daemon/lifetime/heartbeat.js +10 -0
  52. package/dist/command/daemon/lifetime/log-cap.d.ts +68 -0
  53. package/dist/command/daemon/lifetime/log-cap.js +115 -0
  54. package/dist/command/setup/agent-notice-memory.d.ts +16 -0
  55. package/dist/command/setup/agent-notice-memory.js +65 -0
  56. package/dist/command/setup/bringup/dev-server-wait.d.ts +1 -0
  57. package/dist/command/setup/bringup/dev-server-wait.js +16 -0
  58. package/dist/command/setup/bringup/ensure-daemon.js +1 -1
  59. package/dist/command/setup/init/init-host.js +8 -0
  60. package/dist/command/setup/init/init-runtime.js +13 -1
  61. package/dist/command/setup/node-effects.js +1 -1
  62. package/dist/command/setup/probe/page-probe.js +9 -2
  63. package/dist/command/setup/run-setup.d.ts +25 -7
  64. package/dist/command/setup/run-setup.js +51 -9
  65. package/dist/command/setup/setup-command.js +31 -3
  66. package/dist/command/setup/terminal/confirm.js +2 -1
  67. package/dist/command/update/updater.d.ts +27 -0
  68. package/dist/command/update/updater.js +35 -2
  69. package/dist/features/crawl/crawl-tools.js +3 -1
  70. package/dist/features/crawl/crawl.d.ts +2 -0
  71. package/dist/features/crawl/crawl.js +3 -1
  72. package/dist/features/harness/drive-report.d.ts +73 -0
  73. package/dist/features/harness/drive-report.js +196 -0
  74. package/dist/features/harness/driver.js +29 -8
  75. package/dist/features/harness/drivers.d.ts +19 -0
  76. package/dist/features/harness/drivers.js +19 -0
  77. package/dist/features/harness/fill-values.d.ts +67 -0
  78. package/dist/features/harness/fill-values.js +153 -0
  79. package/dist/features/harness/jev-driver.d.ts +188 -0
  80. package/dist/features/harness/jev-driver.js +661 -0
  81. package/dist/features/harness/openai-driver.d.ts +46 -0
  82. package/dist/features/harness/openai-driver.js +197 -0
  83. package/dist/features/harness/platform-config.d.ts +62 -0
  84. package/dist/features/harness/platform-config.js +97 -0
  85. package/dist/features/visual/visual-tools.js +26 -2
  86. package/dist/index.d.ts +1 -1
  87. package/dist/index.js +80 -130
  88. package/dist/judgement/capsule/capsule-store.d.ts +31 -2
  89. package/dist/judgement/capsule/capsule-store.js +58 -6
  90. package/dist/judgement/capsule/capsule.js +5 -0
  91. package/dist/judgement/capsule/causal-summary.d.ts +13 -1
  92. package/dist/judgement/capsule/causal-summary.js +7 -0
  93. package/dist/judgement/domain/domain-model.js +38 -9
  94. package/dist/judgement/domain/domain-tools.js +4 -2
  95. package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
  96. package/dist/judgement/phenomena/phenomena.d.ts +2 -1
  97. package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
  98. package/dist/judgement/runs/artifact/gap-report.js +46 -0
  99. package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
  100. package/dist/judgement/runs/artifact/gap-summary.js +85 -0
  101. package/dist/judgement/runs/artifact/render-report.js +4 -1
  102. package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
  103. package/dist/judgement/runs/artifact/to-artifact.js +1 -1
  104. package/dist/judgement/runs/context-tools.js +21 -3
  105. package/dist/judgement/runs/drive-run.js +7 -2
  106. package/dist/judgement/runs/verification-sync.d.ts +2 -2
  107. package/dist/judgement/runs/verification-sync.js +9 -2
  108. package/dist/language/flows/affected-tools.js +6 -3
  109. package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
  110. package/dist/language/flows/annotate-notes/annotate.js +66 -18
  111. package/dist/language/flows/flow-anchor.d.ts +12 -0
  112. package/dist/language/flows/flow-anchor.js +20 -0
  113. package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
  114. package/dist/language/flows/flow-expect-grammar.js +92 -11
  115. package/dist/language/flows/flow-intent.js +1 -1
  116. package/dist/language/flows/flow-learning.d.ts +3 -0
  117. package/dist/language/flows/flow-learning.js +39 -8
  118. package/dist/language/flows/flow-replay-run.d.ts +55 -10
  119. package/dist/language/flows/flow-replay-run.js +217 -58
  120. package/dist/language/flows/flow-replay-types.d.ts +9 -0
  121. package/dist/language/flows/flow-replay.d.ts +29 -11
  122. package/dist/language/flows/flow-replay.js +221 -203
  123. package/dist/language/flows/flow-result.d.ts +18 -1
  124. package/dist/language/flows/flow-result.js +18 -2
  125. package/dist/language/flows/flow-step-runners.d.ts +26 -10
  126. package/dist/language/flows/flow-step-runners.js +20 -4
  127. package/dist/language/flows/flow-store-for-session.d.ts +2 -1
  128. package/dist/language/flows/flow-success.d.ts +21 -6
  129. package/dist/language/flows/flow-success.js +50 -138
  130. package/dist/language/flows/flow-tools.js +8 -4
  131. package/dist/language/flows/flows.d.ts +15 -10
  132. package/dist/language/flows/flows.js +17 -4
  133. package/dist/language/flows/heal.d.ts +9 -1
  134. package/dist/language/flows/heal.js +24 -2
  135. package/dist/language/flows/learned-sources.d.ts +21 -0
  136. package/dist/language/flows/learned-sources.js +84 -0
  137. package/dist/language/flows/narrow-write.d.ts +4 -4
  138. package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
  139. package/dist/language/flows/recording/recording-backtrack.js +1 -1
  140. package/dist/language/flows/recording/replay-halt.d.ts +2 -1
  141. package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
  142. package/dist/language/flows/replay.js +13 -9
  143. package/dist/language/flows/stores/annotation-store.d.ts +5 -5
  144. package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
  145. package/dist/language/flows/stores/flake-store.js +2 -3
  146. package/dist/language/flows/suite/coverage.d.ts +1 -1
  147. package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
  148. package/dist/language/flows/suite-selection.d.ts +15 -1
  149. package/dist/language/flows/suite-selection.js +1 -1
  150. package/dist/language/flows/verify-change-tools.js +21 -4
  151. package/dist/memory/cloud/account-state.d.ts +1 -1
  152. package/dist/memory/cloud/account-state.js +5 -4
  153. package/dist/memory/cloud/cloud-config.d.ts +9 -0
  154. package/dist/memory/cloud/cloud-config.js +9 -0
  155. package/dist/memory/cloud/cloud-sync.d.ts +7 -4
  156. package/dist/memory/cloud/cloud-sync.js +11 -6
  157. package/dist/memory/cloud/harness-config.d.ts +12 -0
  158. package/dist/memory/cloud/harness-config.js +58 -0
  159. package/dist/memory/cloud/harness-offer.d.ts +44 -0
  160. package/dist/memory/cloud/harness-offer.js +146 -0
  161. package/dist/memory/cloud/harness-switch.d.ts +11 -0
  162. package/dist/memory/cloud/harness-switch.js +53 -0
  163. package/dist/memory/cloud/sync-cycle.d.ts +12 -0
  164. package/dist/memory/cloud/sync-cycle.js +29 -4
  165. package/dist/memory/cloud/sync-daemon.d.ts +35 -2
  166. package/dist/memory/cloud/sync-daemon.js +45 -0
  167. package/dist/memory/cloud/sync-disk.js +11 -6
  168. package/dist/memory/impact/impact-recorder.d.ts +8 -0
  169. package/dist/memory/impact/impact-recorder.js +2 -1
  170. package/dist/memory/impact/impact-store.d.ts +5 -0
  171. package/dist/memory/impact/impact-store.js +25 -0
  172. package/dist/memory/intent/inline-intent.js +8 -3
  173. package/dist/memory/intent/intent-shard-store.d.ts +33 -47
  174. package/dist/memory/intent/intent-shard-store.js +253 -141
  175. package/dist/memory/intent/intent-shard.d.ts +22 -23
  176. package/dist/memory/intent/intent-shard.js +1 -1
  177. package/dist/memory/intent/intent-store.d.ts +13 -8
  178. package/dist/memory/intent/intent-store.js +46 -90
  179. package/dist/memory/intent/intent-tools.js +34 -23
  180. package/dist/memory/journal/ambient-store.js +2 -3
  181. package/dist/memory/journal/attach-journal.d.ts +11 -0
  182. package/dist/memory/journal/attach-journal.js +1 -1
  183. package/dist/memory/journal/deviation-report.js +4 -1
  184. package/dist/memory/journal/deviation-service.js +10 -2
  185. package/dist/memory/journal/drive-flow.d.ts +2 -2
  186. package/dist/memory/journal/drive-flow.js +21 -12
  187. package/dist/memory/journal/envelope-store.js +2 -3
  188. package/dist/memory/journal/journal-query.d.ts +35 -1
  189. package/dist/memory/journal/journal-query.js +46 -0
  190. package/dist/memory/journal/journal-recorder.d.ts +71 -2
  191. package/dist/memory/journal/journal-recorder.js +45 -7
  192. package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
  193. package/dist/memory/journal/on-disk/retain-policy.js +44 -0
  194. package/dist/memory/journal/on-disk/retention.d.ts +91 -2
  195. package/dist/memory/journal/on-disk/retention.js +181 -11
  196. package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
  197. package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
  198. package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -2
  199. package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -38
  200. package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
  201. package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
  202. package/dist/memory/journal/route-template.d.ts +22 -0
  203. package/dist/memory/journal/route-template.js +44 -0
  204. package/dist/memory/journal/self-observation.d.ts +3 -0
  205. package/dist/memory/journal/self-observation.js +67 -0
  206. package/dist/memory/journal/session-end.d.ts +17 -4
  207. package/dist/memory/journal/session-end.js +85 -7
  208. package/dist/memory/journal/session-journal.d.ts +122 -5
  209. package/dist/memory/journal/session-journal.js +489 -32
  210. package/dist/memory/project/artifact-root-resolver.d.ts +3 -0
  211. package/dist/memory/project/artifact-root-resolver.js +89 -0
  212. package/dist/memory/project/artifact-root.d.ts +39 -2
  213. package/dist/memory/project/artifact-root.js +58 -1
  214. package/dist/memory/project/baselines.d.ts +1 -1
  215. package/dist/memory/project/dir/fill-value-store.d.ts +32 -0
  216. package/dist/memory/project/dir/fill-value-store.js +88 -0
  217. package/dist/memory/project/dir/reticle-dir.d.ts +15 -5
  218. package/dist/memory/project/dir/reticle-dir.js +11 -0
  219. package/dist/memory/project/fs/fs-port.d.ts +22 -1
  220. package/dist/memory/project/fs/fs-port.js +72 -5
  221. package/dist/memory/project/fs/write-atomic.d.ts +26 -0
  222. package/dist/memory/project/fs/write-atomic.js +48 -0
  223. package/dist/memory/project/learned-routes.d.ts +16 -2
  224. package/dist/memory/project/learned-routes.js +26 -10
  225. package/dist/memory/project/project-for-root.d.ts +30 -0
  226. package/dist/memory/project/project-for-root.js +43 -0
  227. package/dist/memory/project/project-tools.js +10 -5
  228. package/dist/memory/project/session-root.d.ts +35 -6
  229. package/dist/memory/project/session-root.js +70 -8
  230. package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
  231. package/dist/memory/recall/prior/connection-memory.js +8 -0
  232. package/dist/memory/recall/registered-projects.d.ts +2 -1
  233. package/dist/portal/bridge/auth-failure-reason.d.ts +9 -0
  234. package/dist/portal/bridge/auth-failure-reason.js +20 -3
  235. package/dist/portal/bridge/bridge.d.ts +6 -1
  236. package/dist/portal/bridge/bridge.js +36 -2
  237. package/dist/portal/input/real-input.js +2 -1
  238. package/dist/portal/pool/browser-pool.d.ts +10 -0
  239. package/dist/portal/pool/browser-pool.js +10 -0
  240. package/dist/portal/pool/playwright-launcher.js +1 -0
  241. package/dist/portal/pool/pool-contract.d.ts +2 -0
  242. package/dist/portal/pool/storage-seed.d.ts +2 -1
  243. package/dist/portal/pool/zero-install.d.ts +27 -0
  244. package/dist/portal/pool/zero-install.js +54 -0
  245. package/dist/portal/realm/web-realm.js +6 -0
  246. package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
  247. package/dist/portal/session/dev-server/served-document.d.ts +21 -0
  248. package/dist/portal/session/dev-server/served-document.js +57 -0
  249. package/dist/portal/session/facts/background-traffic.d.ts +9 -0
  250. package/dist/portal/session/facts/background-traffic.js +25 -0
  251. package/dist/portal/session/facts/session-replaced.d.ts +19 -0
  252. package/dist/portal/session/facts/session-replaced.js +22 -0
  253. package/dist/portal/session/live-control-tools.js +48 -3
  254. package/dist/portal/session/no-session-diagnosis.d.ts +1 -0
  255. package/dist/portal/session/no-session-diagnosis.js +67 -60
  256. package/dist/portal/session/no-session-next-action.js +3 -2
  257. package/dist/portal/session/no-session-watch.js +14 -10
  258. package/dist/portal/session/presence/attachment-history.d.ts +13 -2
  259. package/dist/portal/session/rebind.d.ts +18 -0
  260. package/dist/portal/session/rebind.js +38 -0
  261. package/dist/portal/session/session-health.d.ts +61 -0
  262. package/dist/portal/session/session-health.js +40 -1
  263. package/dist/portal/session/session-info.d.ts +6 -0
  264. package/dist/portal/session/session-manager.d.ts +13 -0
  265. package/dist/portal/session/session-manager.js +42 -3
  266. package/dist/portal/session/session-successor.d.ts +11 -0
  267. package/dist/portal/session/session-successor.js +14 -1
  268. package/dist/portal/session/session-verdict-facts.d.ts +28 -0
  269. package/dist/portal/session/session-verdict-facts.js +15 -0
  270. package/dist/portal/session/session.d.ts +26 -10
  271. package/dist/portal/session/session.js +48 -49
  272. package/dist/status-payload.d.ts +12 -1
  273. package/dist/status-payload.js +4 -1
  274. package/dist/surface/http-server.js +3 -5
  275. package/dist/surface/mcp/faults/mcp-outage.d.ts +2 -1
  276. package/dist/surface/mcp/faults/mcp-outage.js +20 -4
  277. package/dist/surface/mcp/mcp-proxy.d.ts +10 -1
  278. package/dist/surface/mcp/mcp-proxy.js +23 -12
  279. package/dist/surface/mcp/mcp.d.ts +10 -1
  280. package/dist/surface/mcp/mcp.js +59 -29
  281. package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +15 -2
  282. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +22 -5
  283. package/dist/surface/mcp/proxy/proxy-log.d.ts +5 -11
  284. package/dist/surface/mcp/proxy/proxy-log.js +7 -22
  285. package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
  286. package/dist/surface/mcp/server-instructions.js +12 -1
  287. package/dist/surface/tools/act/act-preflight.js +6 -1
  288. package/dist/surface/tools/act/already-true.d.ts +40 -0
  289. package/dist/surface/tools/act/already-true.js +36 -0
  290. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
  291. package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
  292. package/dist/surface/tools/act/sequence-grade.js +13 -0
  293. package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
  294. package/dist/surface/tools/act/settle-in-flight.js +29 -6
  295. package/dist/surface/tools/act-capsule.js +49 -8
  296. package/dist/surface/tools/act-tools.js +25 -24
  297. package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
  298. package/dist/surface/tools/args/numeric-bounds.js +30 -3
  299. package/dist/surface/tools/assert/assert-grade.js +14 -2
  300. package/dist/surface/tools/assert/assert-verdict.js +74 -21
  301. package/dist/surface/tools/browser-tools.js +4 -3
  302. package/dist/surface/tools/coverage-tools.js +5 -2
  303. package/dist/surface/tools/dynamic-tools.js +43 -46
  304. package/dist/surface/tools/error-recovery.d.ts +6 -0
  305. package/dist/surface/tools/error-recovery.js +15 -0
  306. package/dist/surface/tools/explore-tools.js +80 -7
  307. package/dist/surface/tools/feedback-tools.d.ts +2 -1
  308. package/dist/surface/tools/feedback-tools.js +50 -3
  309. package/dist/surface/tools/first-sentence.d.ts +24 -0
  310. package/dist/surface/tools/first-sentence.js +0 -0
  311. package/dist/surface/tools/harness-explore.d.ts +125 -1
  312. package/dist/surface/tools/harness-explore.js +401 -15
  313. package/dist/surface/tools/harness-plan.d.ts +76 -0
  314. package/dist/surface/tools/harness-plan.js +156 -0
  315. package/dist/surface/tools/harness-toolset.js +12 -1
  316. package/dist/surface/tools/invoke-tool.js +82 -9
  317. package/dist/surface/tools/lease-hint.d.ts +8 -0
  318. package/dist/surface/tools/lease-hint.js +10 -0
  319. package/dist/surface/tools/lease-tools.d.ts +16 -28
  320. package/dist/surface/tools/lease-tools.js +53 -5
  321. package/dist/surface/tools/lineage-tools.js +1 -1
  322. package/dist/surface/tools/live-call-text.d.ts +6 -0
  323. package/dist/surface/tools/live-call-text.js +36 -2
  324. package/dist/surface/tools/observe-tools.js +69 -15
  325. package/dist/surface/tools/read/query-windowed.d.ts +1 -1
  326. package/dist/surface/tools/read/query-windowed.js +1 -1
  327. package/dist/surface/tools/read-tools.js +1 -2
  328. package/dist/surface/tools/real-input-attempt.d.ts +0 -5
  329. package/dist/surface/tools/real-input-attempt.js +1 -1
  330. package/dist/surface/tools/tool-kit.d.ts +23 -3
  331. package/dist/surface/tools/tool-kit.js +6 -1
  332. package/dist/surface/tools/tool-surface.js +17 -5
  333. package/dist/surface/tools/tools-helpers.d.ts +8 -7
  334. package/dist/surface/tools/tools-helpers.js +11 -13
  335. package/dist/surface/tools/tools.js +70 -30
  336. package/dist/telemetry/bug-found.js +32 -2
  337. package/dist/telemetry/feedback-context.js +3 -3
  338. package/dist/telemetry/feedback-local.d.ts +1 -0
  339. package/dist/telemetry/feedback-local.js +1 -3
  340. package/dist/telemetry/feedback.d.ts +1 -1
  341. package/dist/telemetry/harness-drive.d.ts +6 -0
  342. package/dist/telemetry/harness-drive.js +39 -0
  343. package/dist/telemetry/hud-metrics.d.ts +32 -0
  344. package/dist/telemetry/hud-metrics.js +95 -0
  345. package/dist/telemetry/mcp-connection.d.ts +14 -1
  346. package/dist/telemetry/mcp-connection.js +17 -2
  347. package/dist/telemetry/onboarding-firsts.d.ts +30 -1
  348. package/dist/telemetry/onboarding-firsts.js +54 -12
  349. package/dist/telemetry/session-metrics.d.ts +5 -0
  350. package/dist/telemetry/session-metrics.js +15 -1
  351. package/dist/telemetry/verification-of.js +31 -3
  352. package/dist/wire-journal.d.ts +31 -0
  353. package/dist/wire-journal.js +100 -0
  354. package/docs/README.md +1 -0
  355. package/docs/agent-cheatsheet.md +7 -3
  356. package/docs/autodrive.md +42 -2
  357. package/docs/benchmarks.md +1 -1
  358. package/docs/capabilities.mdx +1 -1
  359. package/docs/cli/cloud.mdx +11 -9
  360. package/docs/cli/doctor.mdx +1 -0
  361. package/docs/cli/init.mdx +1 -0
  362. package/docs/cli/report.mdx +44 -0
  363. package/docs/cli/status.mdx +35 -20
  364. package/docs/cli.mdx +8 -18
  365. package/docs/desktop.mdx +1 -1
  366. package/docs/faq.mdx +1 -1
  367. package/docs/flows.md +11 -0
  368. package/docs/frameworks.mdx +113 -1
  369. package/docs/install-agentic.mdx +36 -0
  370. package/docs/install-manual.mdx +1 -1
  371. package/docs/packages/tauri.mdx +1 -1
  372. package/docs/packages.mdx +1 -1
  373. package/docs/predicates.mdx +144 -4
  374. package/docs/system-map.md +6 -4
  375. package/docs/telemetry.md +2 -1
  376. package/docs/token-efficiency.md +2 -2
  377. package/docs/tools/overview.mdx +1 -1
  378. package/docs/tools/query.mdx +10 -10
  379. package/docs/troubleshooting.mdx +8 -2
  380. package/docs/usage.md +2 -2
  381. package/docs/what-is-recorded.md +62 -0
  382. package/package.json +5 -5
  383. package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
  384. package/dist/judgement/outcome/predicate-to-expect.js +0 -148
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Whether this workspace has taken the three months of free harness, read by the thing that would
3
+ * advertise it.
4
+ *
5
+ * The HUD can only advertise honestly if it knows, and only the platform knows. This is the read —
6
+ * filed beside the other reads of the linked account rather than with the harness itself, because
7
+ * nothing that DRIVES needs it; the thing that needs it is the snapshot the HUD paints from. Same
8
+ * shape as the harness's own `platform-config.ts` and for the same reasons: it is INFORMATION, not a
9
+ * credential, so every failure answers `undefined`, which every caller must treat as "say nothing"
10
+ * rather than as "not claimed". An offline laptop must not start advertising something the person
11
+ * already owns.
12
+ *
13
+ * It is also SYNCHRONOUS at the point of use. The impact snapshot is built inline, dozens of times a
14
+ * session, and the answer changes about once per workspace lifetime, so a cache that refreshes in
15
+ * the background is the whole design: readers get what we last heard, and a stale answer is only
16
+ * ever stale in the safe direction — the card the person is looking at is the one they can act on.
17
+ */
18
+ import { type HarnessOffer } from '@reticlehq/core';
19
+ /** A GET, narrowed to what this file uses, so a test can answer it without a network. */
20
+ export type OfferFetch = (url: string, init: {
21
+ method: string;
22
+ headers: Record<string, string>;
23
+ signal?: AbortSignal;
24
+ }) => Promise<{
25
+ ok: boolean;
26
+ status: number;
27
+ text(): Promise<string>;
28
+ }>;
29
+ /** Ask the platform where this workspace stands. `undefined` means "no answer", never "not claimed". */
30
+ export declare function fetchHarnessOffer(env: Record<string, string | undefined>, claimUrl: string | undefined, doFetch?: OfferFetch, timeoutMs?: number): Promise<HarnessOffer | undefined>;
31
+ /** What the impact snapshot reads: the last answer, with a refresh kicked off when it goes stale. */
32
+ export interface OfferSource {
33
+ read(): HarnessOffer | undefined;
34
+ }
35
+ /**
36
+ * A self-refreshing cache over the read above.
37
+ *
38
+ * Deliberately NOT a timer. A daemon that lives for days would otherwise hold an interval whose only
39
+ * job is to re-ask a question nobody is asking, and it would have to be torn down in a place that
40
+ * already has nine teardowns. Refreshing on a stale READ means the work only happens while somebody
41
+ * is actually looking, and the first read of a process — which is always stale — starts the fetch
42
+ * that the second read answers from.
43
+ */
44
+ export declare function harnessOfferSource(env: Record<string, string | undefined>, claimUrl: () => string | undefined, now?: () => number, load?: (claim: string | undefined) => Promise<HarnessOffer | undefined>): OfferSource;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Whether this workspace has taken the three months of free harness, read by the thing that would
3
+ * advertise it.
4
+ *
5
+ * The HUD can only advertise honestly if it knows, and only the platform knows. This is the read —
6
+ * filed beside the other reads of the linked account rather than with the harness itself, because
7
+ * nothing that DRIVES needs it; the thing that needs it is the snapshot the HUD paints from. Same
8
+ * shape as the harness's own `platform-config.ts` and for the same reasons: it is INFORMATION, not a
9
+ * credential, so every failure answers `undefined`, which every caller must treat as "say nothing"
10
+ * rather than as "not claimed". An offline laptop must not start advertising something the person
11
+ * already owns.
12
+ *
13
+ * It is also SYNCHRONOUS at the point of use. The impact snapshot is built inline, dozens of times a
14
+ * session, and the answer changes about once per workspace lifetime, so a cache that refreshes in
15
+ * the background is the whole design: readers get what we last heard, and a stale answer is only
16
+ * ever stale in the safe direction — the card the person is looking at is the one they can act on.
17
+ */
18
+ import { ReticleEnv, apiKeyFrom } from '@reticlehq/core';
19
+ const OFFER_PATH = '/v1/harness/offer';
20
+ /** Same budget as the model-config read: a drive must never wait on an advert. */
21
+ const TIMEOUT_MS = 2_000;
22
+ /**
23
+ * How long a cached answer is trusted.
24
+ *
25
+ * A claim happens once. The only transition worth catching quickly is the one the person just made
26
+ * in another tab, and ten minutes is well inside the time it takes them to come back and look.
27
+ */
28
+ const FRESH_MS = 10 * 60 * 1_000;
29
+ /**
30
+ * Parse the platform's answer.
31
+ *
32
+ * The platform reports `null` for "never claimed" on the fields it has no value for; the wire
33
+ * contract says ABSENT. They are the same fact and the translation belongs here, at the edge,
34
+ * rather than in a HUD that would then need to know about two spellings of nothing.
35
+ */
36
+ function parse(body, claimUrl) {
37
+ try {
38
+ const parsed = JSON.parse(body);
39
+ if ('object' !== typeof parsed || null === parsed)
40
+ return undefined;
41
+ const record = parsed;
42
+ const claimed = record['claimed'];
43
+ if ('boolean' !== typeof claimed)
44
+ return undefined;
45
+ const offer = { claimed };
46
+ const expiresAt = record['expiresAt'];
47
+ if ('number' === typeof expiresAt)
48
+ offer.expiresAt = expiresAt;
49
+ const days = record['daysRemaining'];
50
+ if ('number' === typeof days)
51
+ offer.daysRemaining = days;
52
+ // The one field that cannot be inferred from the others: a LAPSED claim reports `claimed:false`
53
+ // and `daysRemaining:0`, which is indistinguishable from never having claimed unless the
54
+ // platform's own answer about offerability is carried through.
55
+ const eligible = record['eligible'];
56
+ if ('boolean' === typeof eligible)
57
+ offer.eligible = eligible;
58
+ if (claimUrl !== undefined && 0 < claimUrl.length)
59
+ offer.claimUrl = claimUrl;
60
+ return offer;
61
+ }
62
+ catch {
63
+ return undefined;
64
+ }
65
+ }
66
+ /** Ask the platform where this workspace stands. `undefined` means "no answer", never "not claimed". */
67
+ export async function fetchHarnessOffer(env, claimUrl, doFetch = (url, init) => fetch(url, init), timeoutMs = TIMEOUT_MS) {
68
+ const key = apiKeyFrom(env);
69
+ const host = env[ReticleEnv.CLOUD_URL];
70
+ if (key === undefined || 0 === key.length)
71
+ return undefined;
72
+ if (host === undefined || 0 === host.length)
73
+ return undefined;
74
+ const controller = new AbortController();
75
+ const timer = setTimeout(() => {
76
+ controller.abort();
77
+ }, timeoutMs);
78
+ try {
79
+ const res = await doFetch(`${host.replace(/\/+$/, '')}${OFFER_PATH}`, {
80
+ method: 'GET',
81
+ headers: { authorization: `Bearer ${key}` },
82
+ signal: controller.signal,
83
+ });
84
+ if (!res.ok)
85
+ return undefined;
86
+ return parse(await res.text(), claimUrl);
87
+ }
88
+ catch {
89
+ return undefined;
90
+ }
91
+ finally {
92
+ clearTimeout(timer);
93
+ }
94
+ }
95
+ /**
96
+ * A self-refreshing cache over the read above.
97
+ *
98
+ * Deliberately NOT a timer. A daemon that lives for days would otherwise hold an interval whose only
99
+ * job is to re-ask a question nobody is asking, and it would have to be torn down in a place that
100
+ * already has nine teardowns. Refreshing on a stale READ means the work only happens while somebody
101
+ * is actually looking, and the first read of a process — which is always stale — starts the fetch
102
+ * that the second read answers from.
103
+ */
104
+ export function harnessOfferSource(env, claimUrl, now = () => Date.now(), load = (claim) => fetchHarnessOffer(env, claim)) {
105
+ let cached;
106
+ // `undefined` rather than 0: "never asked" is a different state from "asked at the epoch", and
107
+ // conflating them made a source with an injected clock never take its first read.
108
+ let fetchedAt;
109
+ let inFlight = false;
110
+ const refresh = () => {
111
+ if (inFlight)
112
+ return;
113
+ inFlight = true;
114
+ void load(claimUrl())
115
+ .then((offer) => {
116
+ // A failed read keeps the LAST good answer rather than blanking the card mid-session: one
117
+ // dropped request is not evidence that the offer changed.
118
+ if (offer !== undefined)
119
+ cached = offer;
120
+ fetchedAt = now();
121
+ })
122
+ .catch(() => {
123
+ fetchedAt = now();
124
+ })
125
+ .finally(() => {
126
+ inFlight = false;
127
+ });
128
+ };
129
+ /*
130
+ * Asked once at construction, not first read.
131
+ *
132
+ * Measured in a browser: a daemon pushes the impact snapshot the INSTANT a session attaches, and
133
+ * on a page nobody drives that is the only push there will ever be. Refreshing on the first read
134
+ * meant that push always carried "we have not heard", the answer landed a moment later with
135
+ * nobody to tell, and the HUD showed nothing for the whole session. Starting the fetch when the
136
+ * daemon starts puts the answer in place before any page can connect.
137
+ */
138
+ refresh();
139
+ return {
140
+ read() {
141
+ if (fetchedAt === undefined || FRESH_MS <= now() - fetchedAt)
142
+ refresh();
143
+ return cached;
144
+ },
145
+ };
146
+ }
@@ -0,0 +1,11 @@
1
+ export type SwitchFetch = (url: string, init: {
2
+ method: string;
3
+ headers: Record<string, string>;
4
+ body: string;
5
+ signal?: AbortSignal;
6
+ }) => Promise<{
7
+ ok: boolean;
8
+ status: number;
9
+ }>;
10
+ /** `true` when the platform accepted it. Callers use this for logging, never to block a person. */
11
+ export declare function writeHarnessSwitch(env: Record<string, string | undefined>, enabled: boolean, doFetch?: SwitchFetch, timeoutMs?: number): Promise<boolean>;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Write the harness switch back to the platform.
3
+ *
4
+ * The panel does not keep this state — it emits an intent and this puts it where the dashboard can
5
+ * see it, so the two surfaces cannot disagree. Only the switch is sent: somebody who chose a
6
+ * provider in the console must not have it reset by a panel that never rendered one. The platform's
7
+ * PUT is what guarantees that, not the verb -- it merges the fields it is given and leaves the rest,
8
+ * which its own spec pins ("leaves the fields it was not sent alone").
9
+ *
10
+ * It says PUT because that is what the platform registers. It said PATCH for one release, against a
11
+ * route that has only ever served GET and PUT, so every write 404'd. Nothing reported it: the call
12
+ * is fire-and-forget and failure here is deliberately silent, so the switch simply sprang back on
13
+ * the next snapshot and looked like a UI that would not stick.
14
+ *
15
+ * Failure is SILENT by design, and that deserves saying plainly rather than being discovered. A
16
+ * dev-only overlay that threw a dialog because a settings write timed out would be worse than one
17
+ * that quietly did not save; the next snapshot re-reads the platform, so a lost write shows up as
18
+ * the switch springing back, which is a truthful thing for it to do.
19
+ */
20
+ import { ReticleEnv, apiKeyFrom } from '@reticlehq/core';
21
+ const CONFIG_PATH = '/v1/model/config';
22
+ /** Same budget as the read beside it: a settings write must never hold anything up. */
23
+ const TIMEOUT_MS = 2_000;
24
+ /** `true` when the platform accepted it. Callers use this for logging, never to block a person. */
25
+ export async function writeHarnessSwitch(env, enabled, doFetch = (url, init) => fetch(url, init), timeoutMs = TIMEOUT_MS) {
26
+ const key = apiKeyFrom(env);
27
+ const host = env[ReticleEnv.CLOUD_URL];
28
+ if (key === undefined || 0 === key.length)
29
+ return false;
30
+ if (host === undefined || 0 === host.length)
31
+ return false;
32
+ const controller = new AbortController();
33
+ const timer = setTimeout(() => {
34
+ controller.abort();
35
+ }, timeoutMs);
36
+ try {
37
+ // No projectId in the body: the key IS project-scoped, and the platform refuses to let a key
38
+ // name somebody else's project. Sending one would be asking for a privilege we do not have.
39
+ const res = await doFetch(`${host.replace(/\/+$/, '')}${CONFIG_PATH}`, {
40
+ method: 'PUT',
41
+ headers: { authorization: `Bearer ${key}`, 'content-type': 'application/json' },
42
+ body: JSON.stringify({ harnessEnabled: enabled }),
43
+ signal: controller.signal,
44
+ });
45
+ return res.ok;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ finally {
51
+ clearTimeout(timer);
52
+ }
53
+ }
@@ -61,8 +61,20 @@ export interface CloudSyncState {
61
61
  lastPullAt?: number;
62
62
  /** The last failure, kept so `reticle sync` can say why it is behind instead of just "0 sent". */
63
63
  lastError?: string;
64
+ /**
65
+ * Run ids this machine has watched the server accept, kept only for the runs still held locally.
66
+ *
67
+ * Read ONLY when the server says its own `knownRunIds` was truncated. A complete list is a
68
+ * complete answer — a run missing from one is genuinely missing and must be re-sent — so this
69
+ * fills the gap the server declared and never overrides the server on a question it answered.
70
+ */
71
+ sentRunIds?: string[];
64
72
  }
65
73
  export interface SyncReport {
74
+ /**
75
+ * False if the cycle failed or any offered run was rejected. A completed HTTP exchange alone
76
+ * is not a successful push. Accepted counts and rejection reasons still describe partial success.
77
+ */
66
78
  ok: boolean;
67
79
  /** Runs the server accepted this cycle. */
68
80
  runsSent: number;
@@ -99,9 +99,20 @@ export async function runSyncCycle(deps) {
99
99
  * something the server does not have. Silent data loss is the one failure this protocol must
100
100
  * not have, and an unreadable answer has to mean "it knows nothing", never "it knows this".
101
101
  */
102
- const known = new Set(Array.isArray(held.knownRunIds)
103
- ? held.knownRunIds.filter((id) => 'string' === typeof id)
104
- : []);
102
+ const onlyStrings = (value) => Array.isArray(value) ? value.filter((id) => 'string' === typeof id) : [];
103
+ /*
104
+ * The server may answer "here are the ids I hold, and there are more than I listed". Read as the
105
+ * whole truth, that short list made every run past the server's page look unsent, so a project
106
+ * with enough runs to truncate it re-uploaded its history on every single cycle, forever. The
107
+ * field was declared on this response and read nowhere.
108
+ *
109
+ * Only then is the machine's own record consulted, and it is subject to the same rule as the
110
+ * server's: anything that is not a list of strings reads as "nothing is known".
111
+ */
112
+ const known = new Set([
113
+ ...onlyStrings(held.knownRunIds),
114
+ ...(true === held.truncated ? onlyStrings(deps.state.sentRunIds) : []),
115
+ ]);
105
116
  // Same rule for the hashes: only a string can equal a hash we computed, so anything else reads
106
117
  // as "unknown" and the record is sent once. Re-sending costs a request; skipping costs the data.
107
118
  const hashes = isRecord(held.stateHashes) ? held.stateHashes : {};
@@ -158,7 +169,20 @@ export async function runSyncCycle(deps) {
158
169
  ? runs['rejected']
159
170
  : [];
160
171
  nextState.lastPushAt = deps.now();
172
+ // Accepted means the server has it. A rejected run was refused by index, so it is exactly as
173
+ // unsent as it was before and must never be remembered as delivered.
174
+ const refused = new Set(runsRejected.map((r) => r.index));
175
+ unsent.forEach((run, index) => {
176
+ if (!refused.has(index))
177
+ known.add(run.runId);
178
+ });
161
179
  }
180
+ /*
181
+ * Bounded by the runs still on disk rather than by a cap. Retention already decides how many
182
+ * runs a workspace keeps, and an id for a run nobody holds any more cannot stop an upload that
183
+ * will never be attempted — so the record follows the artifacts and needs no number of its own.
184
+ */
185
+ nextState.sentRunIds = allRuns.map((r) => r.runId).filter((id) => known.has(id));
162
186
  // 3. COLLECT — always, even when there was nothing to send.
163
187
  const query = nextState.cursor === undefined ? '' : `?since=${encodeURIComponent(nextState.cursor)}`;
164
188
  const pull = await call(`${SYNC_PULL_PATH}${query}`);
@@ -199,7 +223,8 @@ export async function runSyncCycle(deps) {
199
223
  delete nextState.lastError;
200
224
  deps.sink.writeState(nextState);
201
225
  return {
202
- ok: true,
226
+ // Keep transport errors separate: rejection reasons already identify the failed artifacts.
227
+ ok: 0 === runsRejected.length,
203
228
  runsSent,
204
229
  runsRejected,
205
230
  flowsSent,
@@ -1,5 +1,6 @@
1
1
  import { type SyncReport } from './sync-cycle.js';
2
- import type { ProjectCloud } from './cloud-config.js';
2
+ import { type ProjectCloud } from './cloud-config.js';
3
+ import type { FileSystemPort } from '../project/fs/fs-port.js';
3
4
  interface SyncDaemonDeps {
4
5
  reticleRoot: string;
5
6
  /** Resolved per tick, not once: a repo linked while the daemon is alive starts syncing itself. */
@@ -29,7 +30,7 @@ interface SyncDaemonDeps {
29
30
  text: string;
30
31
  }>;
31
32
  }
32
- interface SyncDaemon {
33
+ export interface SyncDaemon {
33
34
  /** Run one cycle now, whatever the timer is doing. Returns undefined when not linked. */
34
35
  syncNow: () => Promise<SyncReport | undefined>;
35
36
  /**
@@ -44,10 +45,42 @@ interface SyncDaemon {
44
45
  */
45
46
  nudge: () => void;
46
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>;
47
61
  }
48
62
  /**
49
63
  * Start the loop. Safe to call for an UNLINKED project: it resolves the link on every tick and does
50
64
  * nothing until one appears, which is what lets `reticle link` take effect without a restart.
51
65
  */
52
66
  export declare function startSyncDaemon(deps: SyncDaemonDeps): SyncDaemon;
67
+ /**
68
+ * Start cloud sync for a daemon and hand back the two hooks the daemon wires to it.
69
+ *
70
+ * The wiring lived in `index.ts`, which sits exactly on the thousand-line cap — so the next thing
71
+ * anyone needed there could not be added without a split, and this is the block that most obviously
72
+ * did not belong in a bootstrap file. Four calls that are all one concern: what to sync, where else
73
+ * to look, how a landed run wakes it, and what the panel's button does.
74
+ *
75
+ * Returns the daemon itself: the caller still owns when to nudge, when to sync now, and when to
76
+ * stop. Only the CONSTRUCTION moved, which is the part that was four interdependent arguments about
77
+ * resolving a credential — and the part a bootstrap file has no business spelling out.
78
+ */
79
+ export declare function attachCloudSync(deps: {
80
+ fs: FileSystemPort;
81
+ reticleRoot: string;
82
+ homeDir: string;
83
+ env: NodeJS.ProcessEnv;
84
+ otherRoots: () => readonly string[];
85
+ }): SyncDaemon;
53
86
  export {};
@@ -25,6 +25,7 @@ import { log } from '../../log.js';
25
25
  import { emitSyncHook } from '../../hooks/hook-emit.js';
26
26
  import { describeSync, runSyncCycle } from './sync-cycle.js';
27
27
  import { diskSink, diskSource, readCloudState } from './sync-disk.js';
28
+ import { resolveProjectCloud } from './cloud-config.js';
28
29
  /** How often the daemon cycles. See the note above on why this is a constant and not a curve. */
29
30
  const DAEMON_SYNC_INTERVAL_MS = 60_000;
30
31
  /**
@@ -53,6 +54,15 @@ const FIRST_CYCLE_DELAY_MS = 5_000;
53
54
  * written just after, so cycling on the very first byte would ship a bundle that is missing them.
54
55
  */
55
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;
56
66
  const defaultRequest = async (url, init) => {
57
67
  const res = await fetch(url, init);
58
68
  return { status: res.status, text: await res.text() };
@@ -269,5 +279,40 @@ export function startSyncDaemon(deps) {
269
279
  clearTimeout(timer);
270
280
  timer = undefined;
271
281
  },
282
+ flush: async () => {
283
+ stopped = true;
284
+ if (timer !== undefined)
285
+ clearTimeout(timer);
286
+ timer = undefined;
287
+ // A cycle already running may have started BEFORE the write being flushed, so its report says
288
+ // nothing about that artifact. Wait it out rather than skipping on the overlap guard.
289
+ const deadline = Date.now() + FLUSH_WAIT_MS;
290
+ while (running && Date.now() < deadline)
291
+ await new Promise((resolve) => setTimeout(resolve, FLUSH_POLL_MS));
292
+ await cycle();
293
+ },
272
294
  };
273
295
  }
296
+ /**
297
+ * Start cloud sync for a daemon and hand back the two hooks the daemon wires to it.
298
+ *
299
+ * The wiring lived in `index.ts`, which sits exactly on the thousand-line cap — so the next thing
300
+ * anyone needed there could not be added without a split, and this is the block that most obviously
301
+ * did not belong in a bootstrap file. Four calls that are all one concern: what to sync, where else
302
+ * to look, how a landed run wakes it, and what the panel's button does.
303
+ *
304
+ * Returns the daemon itself: the caller still owns when to nudge, when to sync now, and when to
305
+ * stop. Only the CONSTRUCTION moved, which is the part that was four interdependent arguments about
306
+ * resolving a credential — and the part a bootstrap file has no business spelling out.
307
+ */
308
+ export function attachCloudSync(deps) {
309
+ return startSyncDaemon({
310
+ reticleRoot: deps.reticleRoot,
311
+ cloud: () => resolveProjectCloud(deps.fs, deps.reticleRoot, deps.homeDir, deps.env),
312
+ // Every OTHER linked repo on this machine, not just the directory the daemon was started in.
313
+ // One daemon serves many projects, and pushing only its own root left the rest silently
314
+ // reporting nothing — indistinguishable, on the dashboard, from nobody having verified anything.
315
+ otherRoots: () => Promise.resolve(deps.otherRoots()),
316
+ cloudFor: (root) => resolveProjectCloud(deps.fs, root, deps.homeDir, deps.env),
317
+ });
318
+ }
@@ -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. */
@@ -73,8 +73,8 @@ const DERIVED_FILE = {
73
73
  intent: ReticleDir.INTENT_FILE,
74
74
  };
75
75
  /** The directory the sharded intent store writes into, beside the legacy flat file. */
76
- const INTENT_SUBDIR = 'intent';
77
- const INTENT_INDEX = 'index.json';
76
+ const INTENT_SUBDIR = ReticleDir.INTENT_SUBDIR;
77
+ const INTENT_INDEX = IntentDir.INDEX_FILE;
78
78
  /**
79
79
  * Every intent this project holds, flat file and shards merged.
80
80
  *
@@ -98,10 +98,15 @@ function readIntent(reticleRoot) {
98
98
  ...(legacy?.intents ?? {}),
99
99
  };
100
100
  let shards = 0;
101
- for (const file of safeReaddir(join(reticleRoot, INTENT_SUBDIR))) {
102
- if (!file.endsWith(JSON_SUFFIX) || INTENT_INDEX === file)
101
+ for (const entry of safeReaddir(join(reticleRoot, INTENT_SUBDIR))) {
102
+ if (INTENT_INDEX === entry)
103
103
  continue;
104
- const shard = readJson(join(reticleRoot, INTENT_SUBDIR, file));
104
+ // `<subject>/intent.json` is the layout; a bare `<subject>.json` is an interim shard the store
105
+ // has not migrated yet, and is still this project's memory until it has.
106
+ const path = entry.endsWith(JSON_SUFFIX)
107
+ ? join(reticleRoot, INTENT_SUBDIR, entry)
108
+ : join(reticleRoot, INTENT_SUBDIR, entry, IntentDir.SHARD_FILE);
109
+ const shard = readJson(path);
105
110
  // A hand-edited or half-written shard costs one subject, never the whole sync.
106
111
  if (shard?.intents === undefined)
107
112
  continue;
@@ -1,4 +1,5 @@
1
1
  import { type ImpactCounts, type ImpactDefect, type ImpactSnapshot } from '@reticlehq/core';
2
+ import type { ConfigSource } from '../cloud/harness-config.js';
2
3
  import { ImpactStore, type ImpactFoldMeta } from './impact-store.js';
3
4
  /**
4
5
  * Wire the DEFAULT root — the daemon's own. Idempotent; the first one wins.
@@ -8,6 +9,13 @@ import { ImpactStore, type ImpactFoldMeta } from './impact-store.js';
8
9
  * project passes one, and this is the fallback for calls that do not.
9
10
  */
10
11
  export declare function initImpact(opts: {
12
+ /**
13
+ * The harness-config source, supplied by whoever wired the daemon.
14
+ *
15
+ * Passed in rather than built here: reading the platform lives in `features/harness`, and this
16
+ * directory may not reach for it. The daemon owns both sides.
17
+ */
18
+ config?: ConfigSource;
11
19
  reticleRoot: string | undefined;
12
20
  projectName?: string;
13
21
  now?: () => number;
@@ -50,10 +50,11 @@ export function initImpact(opts) {
50
50
  return storeFor(undefined);
51
51
  if (defaultRoot === undefined) {
52
52
  defaultRoot = opts.reticleRoot;
53
- const { projectName, now } = opts;
53
+ const { projectName, now, config } = opts;
54
54
  storeOpts = {
55
55
  ...(projectName === undefined ? {} : { projectName }),
56
56
  ...(now === undefined ? {} : { now }),
57
+ ...(config === undefined ? {} : { config }),
57
58
  };
58
59
  }
59
60
  return storeFor(opts.reticleRoot);
@@ -1,3 +1,5 @@
1
+ import { type OfferSource } from '../cloud/harness-offer.js';
2
+ import type { ConfigSource } from '../cloud/harness-config.js';
1
3
  import { type ImpactCounts, type ImpactDefect, type AccountState, type ImpactScope, type ImpactSnapshot } from '@reticlehq/core';
2
4
  /** Read a scope, tolerating absence and corruption: a broken file starts over rather than throwing. */
3
5
  export declare function readScope(path: string, now: number): ImpactScope;
@@ -37,6 +39,9 @@ export declare class ImpactStore {
37
39
  globalRoot?: string;
38
40
  /** Reads whether this machine is signed in. Injected so the store stays testable and pure-ish. */
39
41
  account?: () => AccountState;
42
+ /** Where the workspace stands with the free harness offer. Injected so no test touches a network. */
43
+ offer?: OfferSource;
44
+ config?: ConfigSource;
40
45
  });
41
46
  /** Notified after every fold, so the HUD can be pushed a fresh snapshot. */
42
47
  onChange(fn: () => void): void;
@@ -2,6 +2,7 @@ import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  import { readAccountState } from '../cloud/account-state.js';
5
+ import { harnessOfferSource } from '../cloud/harness-offer.js';
5
6
  import { ReticleDir, IMPACT_DAILY_BUCKETS, IMPACT_DEFECT_LIMIT, IMPACT_SCHEMA_VERSION, ImpactScopeSchema, addImpactCounts, emptyImpactCounts, emptyImpactRecords, estimateImpactSavings, } from '@reticlehq/core';
6
7
  /**
7
8
  * Where Reticle keeps the record of what it has done for you.
@@ -174,6 +175,9 @@ export class ImpactStore {
174
175
  #timer;
175
176
  #onChange;
176
177
  #account;
178
+ /** Where this workspace stands with the free harness offer. Cached; see harness-offer.ts. */
179
+ #offer;
180
+ #config;
177
181
  constructor(opts) {
178
182
  this.#paths = impactPaths(opts.reticleRoot, opts.globalRoot ?? homedir());
179
183
  // Resolved per snapshot, not cached: a user who runs `reticle login` in another terminal must
@@ -182,6 +186,12 @@ export class ImpactStore {
182
186
  this.#now = opts.now ?? (() => Date.now());
183
187
  this.#projectName = opts.projectName;
184
188
  this.#dashboardUrl = readDashboardUrl(opts.reticleRoot);
189
+ // The claim happens in the console, so the link this project was linked to IS the claim link —
190
+ // built here rather than in the HUD, which has no way to know where this project points.
191
+ this.#offer = opts.offer ?? harnessOfferSource(process.env, () => this.#dashboardUrl);
192
+ // No default: the loader lives in `features/harness` and this file may not reach for it.
193
+ // A store built without one simply reports no harness config, which the HUD renders as no row.
194
+ this.#config = opts.config ?? { read: () => undefined };
185
195
  const now = this.#now();
186
196
  this.#project = readScope(this.#paths.project, now);
187
197
  this.#global = readScope(this.#paths.global, now);
@@ -211,6 +221,21 @@ export class ImpactStore {
211
221
  if (this.#dashboardUrl !== undefined)
212
222
  snap.dashboardUrl = this.#dashboardUrl;
213
223
  snap.account = this.#account();
224
+ const cfgForOffer = this.#config.read();
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;
231
+ // Absent means "we have not heard", which the HUD renders as nothing at all. Never defaulted to
232
+ // `{claimed:false}`: that would advertise the offer to everyone who is offline.
233
+ if (offer !== undefined)
234
+ snap.harnessOffer = offer;
235
+ // Absent stays absent: the HUD reads that as "we have not heard" and renders no control.
236
+ const cfg = this.#config.read();
237
+ if (cfg !== undefined)
238
+ snap.harnessConfig = cfg;
214
239
  return snap;
215
240
  }
216
241
  /** Write now (process exit, tests). */
@@ -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
  *