@reticlehq/server 3.5.0 → 3.6.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 (328) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +16 -8
  3. package/dist/chromium-launch-options.d.ts +3 -1
  4. package/dist/chromium-launch-options.js +2 -1
  5. package/dist/command/cli/adhoc-verdict.d.ts +1 -0
  6. package/dist/command/cli/adhoc-verdict.js +6 -3
  7. package/dist/command/cli/answers/open-note.d.ts +2 -0
  8. package/dist/command/cli/answers/open-note.js +22 -0
  9. package/dist/command/cli/cli-flow-commands.js +4 -8
  10. package/dist/command/cli/cli-parse.d.ts +1 -1
  11. package/dist/command/cli/cli-parse.js +10 -10
  12. package/dist/command/cli/cli-verify.d.ts +1 -1
  13. package/dist/command/cli/cli-verify.js +35 -25
  14. package/dist/command/cli/cloud-cli.js +51 -12
  15. package/dist/command/cli/cloud-login.d.ts +8 -0
  16. package/dist/command/cli/cloud-login.js +36 -2
  17. package/dist/command/cli/demo/demo-page.js +11 -3
  18. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +46 -0
  19. package/dist/command/cli/doctor/browser/chromium-hint.js +99 -1
  20. package/dist/command/cli/drive/drive-attach.d.ts +27 -2
  21. package/dist/command/cli/drive/drive-attach.js +71 -4
  22. package/dist/command/cli/drive/drive-command.d.ts +2 -1
  23. package/dist/command/cli/drive/drive-command.js +13 -7
  24. package/dist/command/cli/lifecycle/daemon-lifecycle.js +4 -1
  25. package/dist/command/cli/ports/port-holder.d.ts +10 -0
  26. package/dist/command/cli/ports/port-holder.js +24 -0
  27. package/dist/command/cli/setup-mcp-cli.js +6 -4
  28. package/dist/command/cli/tutorial.d.ts +2 -2
  29. package/dist/command/cli/tutorial.js +6 -9
  30. package/dist/command/cli.d.ts +6 -0
  31. package/dist/command/cli.js +28 -22
  32. package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
  33. package/dist/command/daemon/binding/daemon-status-probe.js +1 -0
  34. package/dist/command/daemon/daemon-resolve.d.ts +14 -0
  35. package/dist/command/daemon/daemon-resolve.js +33 -2
  36. package/dist/command/setup/agent-writer.d.ts +1 -1
  37. package/dist/command/setup/agent-writer.js +3 -2
  38. package/dist/command/setup/bringup/connect-lease.d.ts +1 -0
  39. package/dist/command/setup/bringup/connect-lease.js +1 -0
  40. package/dist/command/setup/bringup/dev-server-wait.d.ts +8 -0
  41. package/dist/command/setup/bringup/dev-server-wait.js +19 -0
  42. package/dist/command/setup/init/init-port.d.ts +4 -0
  43. package/dist/command/setup/init/init-port.js +10 -0
  44. package/dist/command/setup/init/init-runtime.d.ts +10 -0
  45. package/dist/command/setup/init/init-runtime.js +18 -1
  46. package/dist/command/setup/node-effects.js +2 -1
  47. package/dist/command/setup/remaining-steps.js +2 -1
  48. package/dist/command/setup/run-setup.d.ts +5 -0
  49. package/dist/command/setup/run-setup.js +35 -27
  50. package/dist/command/setup/setup-command.d.ts +2 -1
  51. package/dist/command/setup/setup-command.js +2 -0
  52. package/dist/command/version/version-skew.js +11 -2
  53. package/dist/features/crawl/crawl-tools.js +4 -0
  54. package/dist/features/crawl/crawl.d.ts +6 -0
  55. package/dist/features/crawl/crawl.js +9 -1
  56. package/dist/features/exhaust/ledger.d.ts +7 -0
  57. package/dist/features/exhaust/ledger.js +35 -9
  58. package/dist/features/harness/app-findings.d.ts +2 -0
  59. package/dist/features/harness/app-findings.js +52 -0
  60. package/dist/features/harness/drive-report.d.ts +27 -1
  61. package/dist/features/harness/drive-report.js +162 -11
  62. package/dist/features/harness/drivers.d.ts +10 -14
  63. package/dist/features/harness/drivers.js +10 -14
  64. package/dist/features/harness/fill-values.d.ts +3 -73
  65. package/dist/features/harness/fill-values.js +3 -169
  66. package/dist/features/harness/goals.d.ts +21 -0
  67. package/dist/features/harness/goals.js +77 -0
  68. package/dist/features/harness/harness.d.ts +21 -0
  69. package/dist/features/harness/harness.js +69 -2
  70. package/dist/features/harness/platform/drive-target.d.ts +30 -0
  71. package/dist/features/harness/platform/drive-target.js +81 -0
  72. package/dist/features/harness/platform/local-apps.d.ts +94 -0
  73. package/dist/features/harness/platform/local-apps.js +204 -0
  74. package/dist/features/harness/platform/remote-drive.d.ts +180 -0
  75. package/dist/features/harness/platform/remote-drive.js +365 -0
  76. package/dist/features/harness/platform/script.d.ts +48 -0
  77. package/dist/features/harness/platform/script.js +128 -0
  78. package/dist/features/harness/platform/server-driver.d.ts +41 -0
  79. package/dist/features/harness/platform/server-driver.js +179 -0
  80. package/dist/features/harness/platform-config.d.ts +7 -0
  81. package/dist/features/harness/platform-config.js +14 -2
  82. package/dist/features/harness/script/app-controls.d.ts +5 -0
  83. package/dist/features/harness/script/app-controls.js +40 -0
  84. package/dist/features/harness/script/lane-ids.d.ts +14 -0
  85. package/dist/features/harness/script/lane-ids.js +16 -0
  86. package/dist/features/harness/script/script-run.d.ts +41 -0
  87. package/dist/features/harness/script/script-run.js +299 -0
  88. package/dist/features/license/license.d.ts +2 -2
  89. package/dist/features/visual/visual-tools.d.ts +10 -1
  90. package/dist/features/visual/visual-tools.js +40 -3
  91. package/dist/hooks/driven-by.d.ts +4 -0
  92. package/dist/hooks/driven-by.js +16 -0
  93. package/dist/hooks/hook-commands.js +15 -4
  94. package/dist/hooks/tool-log.d.ts +9 -0
  95. package/dist/hooks/tool-log.js +29 -0
  96. package/dist/index.d.ts +1 -0
  97. package/dist/index.js +76 -82
  98. package/dist/judgement/runs/artifact/build-verification-run.js +6 -0
  99. package/dist/judgement/runs/artifact/replay-mapping.d.ts +1 -1
  100. package/dist/judgement/runs/artifact/replay-mapping.js +16 -6
  101. package/dist/judgement/runs/artifact/run-store.d.ts +1 -1
  102. package/dist/judgement/runs/artifact/run-store.js +26 -1
  103. package/dist/judgement/runs/drive-run.d.ts +22 -0
  104. package/dist/judgement/runs/drive-run.js +99 -1
  105. package/dist/judgement/runs/repair-prompt.js +1 -1
  106. package/dist/judgement/runs/reticle-runner.js +8 -1
  107. package/dist/judgement/runs/verification-sync.d.ts +3 -2
  108. package/dist/judgement/runs/verification-sync.js +4 -3
  109. package/dist/language/flows/change/gate.d.ts +10 -0
  110. package/dist/language/flows/change/gate.js +22 -0
  111. package/dist/language/flows/decision.js +34 -0
  112. package/dist/language/flows/flow-anchor.d.ts +54 -8
  113. package/dist/language/flows/flow-anchor.js +88 -3
  114. package/dist/language/flows/flow-cross-step.d.ts +15 -0
  115. package/dist/language/flows/flow-cross-step.js +21 -0
  116. package/dist/language/flows/flow-learning.js +8 -11
  117. package/dist/language/flows/flow-mutate-tools.js +5 -3
  118. package/dist/language/flows/flow-replay-run.d.ts +23 -9
  119. package/dist/language/flows/flow-replay-run.js +101 -94
  120. package/dist/language/flows/flow-replay.d.ts +23 -1
  121. package/dist/language/flows/flow-replay.js +92 -19
  122. package/dist/language/flows/flow-scope.js +5 -1
  123. package/dist/language/flows/flow-step-runners.d.ts +20 -2
  124. package/dist/language/flows/flow-step-runners.js +236 -42
  125. package/dist/language/flows/flow-store-for-session.d.ts +2 -2
  126. package/dist/language/flows/flow-store-for-session.js +2 -2
  127. package/dist/language/flows/flow-tools.js +58 -34
  128. package/dist/language/flows/flows.js +5 -2
  129. package/dist/language/flows/heal-run.js +6 -4
  130. package/dist/language/flows/recording/flake.d.ts +2 -2
  131. package/dist/language/flows/recording/tape/recordings.d.ts +30 -4
  132. package/dist/language/flows/recording/tape/recordings.js +40 -8
  133. package/dist/language/flows/replay-from-hud.d.ts +20 -0
  134. package/dist/language/flows/replay-from-hud.js +51 -0
  135. package/dist/language/flows/replay.d.ts +9 -5
  136. package/dist/language/flows/replay.js +27 -9
  137. package/dist/language/flows/suite/suite-flakes.js +2 -0
  138. package/dist/language/flows/suite-selection.d.ts +2 -15
  139. package/dist/language/flows/suite-selection.js +4 -3
  140. package/dist/language/flows/verify-change-tools.js +8 -4
  141. package/dist/launch-chromium.d.ts +27 -5
  142. package/dist/launch-chromium.js +38 -8
  143. package/dist/memory/cloud/cloud-config.d.ts +12 -0
  144. package/dist/memory/cloud/cloud-config.js +15 -0
  145. package/dist/memory/cloud/cloud-sync.d.ts +17 -14
  146. package/dist/memory/cloud/cloud-sync.js +12 -1
  147. package/dist/memory/cloud/harness-config.d.ts +26 -1
  148. package/dist/memory/cloud/harness-config.js +99 -4
  149. package/dist/memory/cloud/hud-notices-source.d.ts +21 -0
  150. package/dist/memory/cloud/hud-notices-source.js +128 -0
  151. package/dist/memory/cloud/memory-scope.d.ts +144 -0
  152. package/dist/memory/cloud/memory-scope.js +205 -0
  153. package/dist/memory/cloud/sync-cycle.d.ts +12 -3
  154. package/dist/memory/cloud/sync-cycle.js +16 -3
  155. package/dist/memory/cloud/sync-daemon.js +7 -5
  156. package/dist/memory/cloud/sync-disk.d.ts +1 -1
  157. package/dist/memory/cloud/sync-disk.js +5 -8
  158. package/dist/memory/impact/account-watch.d.ts +19 -0
  159. package/dist/memory/impact/account-watch.js +34 -0
  160. package/dist/memory/impact/impact-recorder.d.ts +8 -0
  161. package/dist/memory/impact/impact-recorder.js +12 -2
  162. package/dist/memory/impact/impact-store.d.ts +16 -0
  163. package/dist/memory/impact/impact-store.js +142 -26
  164. package/dist/memory/intent/intent-shard.d.ts +2 -2
  165. package/dist/memory/intent/intent-tools.js +80 -2
  166. package/dist/memory/journal/envelope.d.ts +8 -8
  167. package/dist/memory/journal/journal-recorder.js +7 -1
  168. package/dist/memory/journal/on-disk/retention.d.ts +3 -0
  169. package/dist/memory/journal/on-disk/retention.js +5 -0
  170. package/dist/memory/journal/on-disk/startup-maintenance.js +3 -1
  171. package/dist/memory/journal/on-disk/workspace-tiers.js +7 -4
  172. package/dist/memory/journal/session-end.js +6 -4
  173. package/dist/memory/project/artifact-root-resolver.d.ts +26 -1
  174. package/dist/memory/project/artifact-root-resolver.js +94 -13
  175. package/dist/memory/project/artifact-root.d.ts +42 -5
  176. package/dist/memory/project/artifact-root.js +87 -8
  177. package/dist/memory/project/dir/reticle-dir.d.ts +4 -2
  178. package/dist/memory/project/dir/reticle-dir.js +3 -1
  179. package/dist/memory/project/project-tools.js +22 -3
  180. package/dist/memory/project/session-root.d.ts +24 -1
  181. package/dist/memory/project/session-root.js +41 -5
  182. package/dist/memory/project/sync-status.d.ts +39 -0
  183. package/dist/memory/project/sync-status.js +90 -0
  184. package/dist/memory/recall/memory-tools.js +2 -0
  185. package/dist/memory/recall/project-memory.d.ts +19 -0
  186. package/dist/memory/recall/project-memory.js +45 -6
  187. package/dist/portal/bridge/bridge.d.ts +3 -1
  188. package/dist/portal/bridge/bridge.js +19 -2
  189. package/dist/portal/bridge/pairing-token.d.ts +9 -0
  190. package/dist/portal/bridge/pairing-token.js +17 -0
  191. package/dist/portal/input/real-input.d.ts +81 -10
  192. package/dist/portal/input/real-input.js +207 -22
  193. package/dist/portal/input/viewport-tools.js +3 -3
  194. package/dist/portal/pool/browser-pool.d.ts +22 -9
  195. package/dist/portal/pool/browser-pool.js +70 -35
  196. package/dist/portal/pool/playwright-launcher.d.ts +13 -0
  197. package/dist/portal/pool/playwright-launcher.js +17 -1
  198. package/dist/portal/pool/pool-contract.d.ts +7 -4
  199. package/dist/portal/session/command-payload.d.ts +8 -0
  200. package/dist/portal/session/command-payload.js +19 -0
  201. package/dist/portal/session/dev-server/dev-command.d.ts +12 -0
  202. package/dist/portal/session/dev-server/dev-command.js +61 -12
  203. package/dist/portal/session/facts/departure-note.d.ts +22 -0
  204. package/dist/portal/session/facts/departure-note.js +47 -0
  205. package/dist/portal/session/facts/session-replaced.d.ts +5 -0
  206. package/dist/portal/session/facts/session-replaced.js +5 -0
  207. package/dist/portal/session/first-run-wiring.d.ts +42 -0
  208. package/dist/portal/session/first-run-wiring.js +132 -0
  209. package/dist/portal/session/human/notes-ledger.d.ts +20 -0
  210. package/dist/portal/session/human/notes-ledger.js +55 -0
  211. package/dist/portal/session/human/review-store.d.ts +12 -0
  212. package/dist/portal/session/human/review-store.js +18 -10
  213. package/dist/portal/session/live-control-tools.d.ts +0 -13
  214. package/dist/portal/session/live-control-tools.js +28 -3
  215. package/dist/portal/session/no-session-diagnosis.d.ts +7 -0
  216. package/dist/portal/session/no-session-diagnosis.js +15 -0
  217. package/dist/portal/session/no-session-next-action.d.ts +11 -0
  218. package/dist/portal/session/no-session-next-action.js +31 -5
  219. package/dist/portal/session/no-session-watch.d.ts +14 -1
  220. package/dist/portal/session/no-session-watch.js +63 -10
  221. package/dist/portal/session/presence/session-mirror.js +3 -0
  222. package/dist/portal/session/serving-directory.d.ts +27 -0
  223. package/dist/portal/session/serving-directory.js +45 -0
  224. package/dist/portal/session/session-health.d.ts +7 -0
  225. package/dist/portal/session/session-health.js +6 -0
  226. package/dist/portal/session/session-info.d.ts +8 -1
  227. package/dist/portal/session/session-info.js +6 -0
  228. package/dist/portal/session/session-manager.d.ts +21 -0
  229. package/dist/portal/session/session-manager.js +44 -8
  230. package/dist/portal/session/session-reaper.js +4 -1
  231. package/dist/portal/session/session-successor.d.ts +5 -0
  232. package/dist/portal/session/session-tools.js +26 -3
  233. package/dist/portal/session/session.d.ts +7 -2
  234. package/dist/portal/session/session.js +21 -18
  235. package/dist/start-options.d.ts +6 -0
  236. package/dist/status-payload.d.ts +10 -0
  237. package/dist/status-payload.js +23 -0
  238. package/dist/surface/http-server.js +11 -1
  239. package/dist/surface/mcp/mcp-proxy.d.ts +5 -0
  240. package/dist/surface/mcp/mcp-proxy.js +36 -5
  241. package/dist/surface/mcp/mcp.d.ts +11 -6
  242. package/dist/surface/mcp/mcp.js +27 -13
  243. package/dist/surface/mcp/server-instructions.d.ts +4 -2
  244. package/dist/surface/mcp/server-instructions.js +14 -4
  245. package/dist/surface/mcp/shared-params.js +2 -2
  246. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +8 -0
  247. package/dist/surface/tools/act/navigation/navigate-arrival.js +56 -2
  248. package/dist/surface/tools/act/settle-in-flight.js +25 -3
  249. package/dist/surface/tools/act-sequence-tool.js +2 -2
  250. package/dist/surface/tools/act-tools.js +23 -7
  251. package/dist/surface/tools/assert/assert-verdict.d.ts +1 -1
  252. package/dist/surface/tools/assert/assert-verdict.js +17 -2
  253. package/dist/surface/tools/browser-tools.js +8 -1
  254. package/dist/surface/tools/chat-drives.d.ts +26 -0
  255. package/dist/surface/tools/chat-drives.js +165 -0
  256. package/dist/surface/tools/empty-session-list.d.ts +41 -0
  257. package/dist/surface/tools/empty-session-list.js +80 -0
  258. package/dist/surface/tools/error-recovery.d.ts +6 -0
  259. package/dist/surface/tools/error-recovery.js +37 -4
  260. package/dist/surface/tools/explore-tools.d.ts +10 -1
  261. package/dist/surface/tools/explore-tools.js +82 -13
  262. package/dist/surface/tools/feedback-tools.js +14 -0
  263. package/dist/surface/tools/harness-explore.d.ts +96 -14
  264. package/dist/surface/tools/harness-explore.js +227 -206
  265. package/dist/surface/tools/harness-plan.d.ts +12 -0
  266. package/dist/surface/tools/harness-plan.js +31 -0
  267. package/dist/surface/tools/harness-script.d.ts +8 -0
  268. package/dist/surface/tools/harness-script.js +342 -0
  269. package/dist/surface/tools/harness-toolset.d.ts +14 -1
  270. package/dist/surface/tools/harness-toolset.js +77 -6
  271. package/dist/surface/tools/invoke-tool.js +52 -1
  272. package/dist/surface/tools/lease-seed.d.ts +29 -0
  273. package/dist/surface/tools/lease-seed.js +123 -0
  274. package/dist/surface/tools/lease-tools.d.ts +3 -23
  275. package/dist/surface/tools/lease-tools.js +30 -131
  276. package/dist/surface/tools/live-call-text.js +2 -2
  277. package/dist/surface/tools/next-step.d.ts +19 -0
  278. package/dist/surface/tools/next-step.js +159 -0
  279. package/dist/surface/tools/observe-tools.js +2 -4
  280. package/dist/surface/tools/platform-moment.d.ts +12 -0
  281. package/dist/surface/tools/platform-moment.js +168 -0
  282. package/dist/surface/tools/query-hint-schema.d.ts +7 -0
  283. package/dist/surface/tools/query-hint-schema.js +10 -0
  284. package/dist/surface/tools/read-tools.js +8 -2
  285. package/dist/surface/tools/real-input-attempt.js +96 -13
  286. package/dist/surface/tools/tool-kit.d.ts +15 -1
  287. package/dist/surface/tools/tool-kit.js +4 -6
  288. package/dist/surface/tools/tools.js +20 -48
  289. package/dist/telemetry/hud-metrics.d.ts +2 -0
  290. package/dist/telemetry/hud-metrics.js +17 -0
  291. package/dist/telemetry/telemetry.d.ts +12 -0
  292. package/dist/telemetry/telemetry.js +43 -14
  293. package/dist/telemetry/verification-of.js +1 -0
  294. package/dist/wire-journal.js +4 -1
  295. package/docs/README.md +1 -1
  296. package/docs/agent-cheatsheet.md +1 -1
  297. package/docs/autodrive.md +29 -25
  298. package/docs/cli/doctor.mdx +6 -0
  299. package/docs/cli/drive.mdx +8 -2
  300. package/docs/cli/init.mdx +11 -13
  301. package/docs/cli/status.mdx +5 -1
  302. package/docs/cli/version.mdx +3 -3
  303. package/docs/cli.mdx +1 -1
  304. package/docs/desktop.mdx +5 -1
  305. package/docs/environment.mdx +3 -7
  306. package/docs/getting-started.md +13 -11
  307. package/docs/human-control.md +1 -2
  308. package/docs/index.mdx +4 -3
  309. package/docs/install-manual.mdx +2 -2
  310. package/docs/packages/tauri.mdx +2 -2
  311. package/docs/packages/vite-plugin.mdx +22 -1
  312. package/docs/packages.mdx +1 -1
  313. package/docs/quickstart.mdx +17 -7
  314. package/docs/telemetry.md +1 -1
  315. package/docs/tools/session-and-feedback.mdx +9 -9
  316. package/docs/usage.md +5 -2
  317. package/docs/what-is-recorded.md +13 -3
  318. package/docs/what-is-reticle.mdx +3 -1
  319. package/package.json +8 -12
  320. package/dist/features/harness/driver.d.ts +0 -50
  321. package/dist/features/harness/driver.js +0 -236
  322. package/dist/features/harness/jev-driver.d.ts +0 -188
  323. package/dist/features/harness/jev-driver.js +0 -678
  324. package/dist/features/harness/openai-driver.d.ts +0 -58
  325. package/dist/features/harness/openai-driver.js +0 -206
  326. package/dist/memory/project/dir/fill-value-store.d.ts +0 -32
  327. package/dist/memory/project/dir/fill-value-store.js +0 -88
  328. package/docs/local-registry.md +0 -143
@@ -39,10 +39,10 @@ const SYNC_PULL_PATH = '/v1/sync/pull';
39
39
  /**
40
40
  * The derived records that ride along with runs, and the file each one lives in.
41
41
  *
42
- * A list rather than three hand-written blocks so adding a fourth is one line and cannot be
43
- * half-done — the bundle, the hashing and the reporting all walk this.
42
+ * One list, so adding a record is one line and cannot be half-done: the bundle, the hashing, the
43
+ * reporting and the disk source (`sync-disk.ts`) all read it.
44
44
  */
45
- const DERIVED_RECORDS = [
45
+ export const DERIVED_RECORDS = [
46
46
  { kind: 'impact', file: ReticleDir.IMPACT_FILE },
47
47
  { kind: 'flake', file: ReticleDir.FLAKE_FILE },
48
48
  { kind: 'intent', file: ReticleDir.INTENT_FILE },
@@ -50,6 +50,10 @@ const DERIVED_RECORDS = [
50
50
  // cannot tell a server a page that drifted from one that always behaved that way.
51
51
  { kind: 'envelopes', file: ReticleDir.ENVELOPES_FILE },
52
52
  { kind: 'assertion-tiers', file: ReticleDir.TIERS_FILE },
53
+ // Reticle Coverage: what the platform's Overview shows as controls proved and routes reached.
54
+ { kind: 'coverage', file: ReticleDir.COVERAGE_FILE },
55
+ // The notes a human pinned in the HUD, pending and resolved: the review trail a team reads.
56
+ { kind: 'notes', file: ReticleDir.NOTES_FILE },
53
57
  ];
54
58
  const asJson = (text) => {
55
59
  try {
@@ -75,6 +79,8 @@ const flowName = (flow, index) => {
75
79
  const FLOW_VERSION_FIX = 'update the platform or remove `compare` from the flow';
76
80
  const RUN_VERSION_FIX = 'update the platform';
77
81
  /** The server's per-part answer: accepted count and rejection list, each read defensively. */
82
+ /** A 200 that never confirmed the runs it was sent. Kept queued, and said, rather than dropped. */
83
+ export const MSG_RUNS_UNCONFIRMED = 'sync 200 without a runs result: the server did not confirm the runs it was sent, so they stay queued';
78
84
  function partResult(body, part) {
79
85
  const raw = isRecord(body[part]) ? body[part] : {};
80
86
  const accepted = 'number' === typeof raw['accepted'] ? raw['accepted'] : 0;
@@ -321,6 +327,13 @@ export async function runSyncCycle(deps) {
321
327
  break;
322
328
  }
323
329
  const answer = isRecord(pushed.json) ? pushed.json : {};
330
+ // Runs were sent and the answer says nothing about them: a proxy's 200 page, an empty body.
331
+ // Delivered means the server said so. Recording them as sent here dropped them for good while
332
+ // the cycle reported ok; they stay queued, and the reason is the cycle's error.
333
+ if (batch.length > 0 && !isRecord(answer['runs'])) {
334
+ pushError = MSG_RUNS_UNCONFIRMED;
335
+ break;
336
+ }
324
337
  const runs = partResult(answer, 'runs');
325
338
  runsSent += runs.accepted;
326
339
  // Accepted means the server has it. A rejected run was refused by index, so it is exactly as
@@ -21,6 +21,7 @@
21
21
  * laptop on a train would otherwise fill the log with the same line four hundred times and teach
22
22
  * everyone to ignore it.
23
23
  */
24
+ import { syncRequest } from './cloud-sync.js';
24
25
  import { log } from '../../log.js';
25
26
  import { emitSyncHook } from '../../hooks/hook-emit.js';
26
27
  import { describeSync, runSyncCycle } from './sync-cycle.js';
@@ -69,10 +70,7 @@ const movedAnything = (report) => report.runsSent > 0 ||
69
70
  report.capsulesSent > 0 ||
70
71
  report.derivedSent.length > 0 ||
71
72
  report.pulled > 0;
72
- const defaultRequest = async (url, init) => {
73
- const res = await fetch(url, init);
74
- return { status: res.status, text: await res.text() };
75
- };
73
+ const defaultRequest = (url, init) => syncRequest(url, init);
76
74
  /**
77
75
  * Start the loop. Safe to call for an UNLINKED project: it resolves the link on every tick and does
78
76
  * nothing until one appears, which is what lets `reticle link` take effect without a restart.
@@ -147,7 +145,11 @@ export function startSyncDaemon(deps) {
147
145
  continue;
148
146
  try {
149
147
  const cloud = await cloudFor(root);
150
- if (null === cloud.config)
148
+ // Linked HERE, by its own link file. An unlinked root resolves to the environment's key,
149
+ // which is the right answer for the root this daemon stands in and somebody else's project
150
+ // for every other one: a daemon started with RETICLE_API_KEY pushed every project this
151
+ // machine had seen into that one, a status and a pull per root on every cycle.
152
+ if (null === cloud.config || null === cloud.projectId)
151
153
  continue;
152
154
  const report = await pushRoot(root, cloud);
153
155
  if (report === undefined)
@@ -1,4 +1,4 @@
1
- import type { CloudSyncState, PulledIssues, SyncSink, SyncSource } from './sync-cycle.js';
1
+ import { type CloudSyncState, type PulledIssues, type SyncSink, type SyncSource } from './sync-cycle.js';
2
2
  /** What the cycle reads, backed by a real `.reticle` directory. */
3
3
  export declare function diskSource(reticleRoot: string): SyncSource;
4
4
  /** What the cycle writes back into `.reticle/`. */
@@ -10,9 +10,11 @@
10
10
  * one unsynced record, not a crashed sync. There is no state here worth defending against a parse
11
11
  * error — the file will be rewritten on the next tool call anyway.
12
12
  */
13
+ import { shareableRun } from '@reticlehq/core/artifacts';
13
14
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync, } from 'node:fs';
14
15
  import { dirname, join } from 'node:path';
15
16
  import { IntentDir, ReticleDir } from '@reticlehq/core';
17
+ import { DERIVED_RECORDS, } from './sync-cycle.js';
16
18
  import { subjectFor } from '../intent/intent-subject.js';
17
19
  const JSON_SUFFIX = '.json';
18
20
  /** Read + parse, or undefined for absent/unreadable/malformed. Never throws. */
@@ -79,13 +81,7 @@ function readFlows(root) {
79
81
  return [];
80
82
  }
81
83
  }
82
- const DERIVED_FILE = {
83
- impact: ReticleDir.IMPACT_FILE,
84
- flake: ReticleDir.FLAKE_FILE,
85
- intent: ReticleDir.INTENT_FILE,
86
- envelopes: ReticleDir.ENVELOPES_FILE,
87
- 'assertion-tiers': ReticleDir.TIERS_FILE,
88
- };
84
+ const DERIVED_FILE = Object.fromEntries(DERIVED_RECORDS.map((record) => [record.kind, record.file]));
89
85
  /** The directory the sharded intent store writes into, beside the legacy flat file. */
90
86
  const INTENT_SUBDIR = ReticleDir.INTENT_SUBDIR;
91
87
  const INTENT_INDEX = IntentDir.INDEX_FILE;
@@ -170,7 +166,8 @@ export function diskSource(reticleRoot) {
170
166
  runs: () => readJsonDir(join(reticleRoot, ReticleDir.RUNS_SUBDIR))
171
167
  .map((payload) => {
172
168
  const id = payload?.runId;
173
- return 'string' === typeof id ? { runId: id, payload } : undefined;
169
+ // The prompt context leaves the machine only when the project opted in.
170
+ return 'string' === typeof id ? { runId: id, payload: shareableRun(payload) } : undefined;
174
171
  })
175
172
  // A run artifact with no id cannot be diffed against the server's list, so sending it would
176
173
  // mean re-sending it every cycle forever. Dropped rather than uploaded repeatedly.
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Repaint the HUD when sign-in or a project link changes on disk.
3
+ *
4
+ * The impact snapshot already reads both files fresh, but it is only PUSHED when a tab attaches,
5
+ * a tool call lands, or the HUD's own Sign in completes. `reticle logout` and `reticle link` run in
6
+ * another process, so neither caused a push: a signed-out machine kept showing the old name, and a
7
+ * freshly linked project kept saying "Not linked", until somebody reloaded or drove something.
8
+ *
9
+ * ponytail: stat polling (`fs.watchFile`), not `fs.watch`. It works on a file that does not exist
10
+ * yet — the usual state of `cloud.json` before a link — and costs one stat per file per interval.
11
+ */
12
+ /** How often each watched file is stat'ed. A second reads as live to somebody switching windows. */
13
+ export declare const ACCOUNT_WATCH_INTERVAL_MS = 1000;
14
+ export interface AccountFilesWatch {
15
+ /** Start watching one file. Watching the same path twice is a no-op. */
16
+ watch(path: string): void;
17
+ close(): void;
18
+ }
19
+ export declare function watchAccountFiles(onChange: () => void, intervalMs?: number): AccountFilesWatch;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Repaint the HUD when sign-in or a project link changes on disk.
3
+ *
4
+ * The impact snapshot already reads both files fresh, but it is only PUSHED when a tab attaches,
5
+ * a tool call lands, or the HUD's own Sign in completes. `reticle logout` and `reticle link` run in
6
+ * another process, so neither caused a push: a signed-out machine kept showing the old name, and a
7
+ * freshly linked project kept saying "Not linked", until somebody reloaded or drove something.
8
+ *
9
+ * ponytail: stat polling (`fs.watchFile`), not `fs.watch`. It works on a file that does not exist
10
+ * yet — the usual state of `cloud.json` before a link — and costs one stat per file per interval.
11
+ */
12
+ import { unwatchFile, watchFile } from 'node:fs';
13
+ /** How often each watched file is stat'ed. A second reads as live to somebody switching windows. */
14
+ export const ACCOUNT_WATCH_INTERVAL_MS = 1_000;
15
+ export function watchAccountFiles(onChange, intervalMs = ACCOUNT_WATCH_INTERVAL_MS) {
16
+ const watched = new Set();
17
+ const listener = () => {
18
+ onChange();
19
+ };
20
+ return {
21
+ watch(path) {
22
+ if (watched.has(path))
23
+ return;
24
+ watched.add(path);
25
+ // Not persistent: a watcher must never be the reason a daemon cannot exit.
26
+ watchFile(path, { persistent: false, interval: intervalMs }, listener);
27
+ },
28
+ close() {
29
+ for (const path of watched)
30
+ unwatchFile(path, listener);
31
+ watched.clear();
32
+ },
33
+ };
34
+ }
@@ -16,9 +16,17 @@ export declare function initImpact(opts: {
16
16
  * directory may not reach for it. The daemon owns both sides.
17
17
  */
18
18
  config?: ConfigSource;
19
+ /** Resolve credentials per project when one daemon serves multiple linked apps. */
20
+ configForRoot?: (root: string) => ConfigSource;
21
+ /** Reticle Coverage for a project root, supplied by the daemon (the ledger is a feature's). */
22
+ coverageForRoot?: (root: string) => () => Record<string, number> | undefined;
19
23
  reticleRoot: string | undefined;
20
24
  projectName?: string;
21
25
  now?: () => number;
26
+ /** Where the machine-wide `~/.reticle` lives. Tests pass a temp dir so they never write the real one. */
27
+ globalRoot?: string;
28
+ /** This build's version, for HUD notices that need a newer SDK. */
29
+ sdkVersion?: string;
22
30
  }): ImpactStore | undefined;
23
31
  /** The live store for a root (default when omitted), if one has been initialised. */
24
32
  export declare function impactStore(root?: string): ImpactStore | undefined;
@@ -31,7 +31,13 @@ function storeFor(root) {
31
31
  return undefined;
32
32
  let found = stores.get(key);
33
33
  if (found === undefined) {
34
- found = new ImpactStore({ ...storeOpts, reticleRoot: key });
34
+ const { configForRoot, coverageForRoot, ...opts } = storeOpts;
35
+ found = new ImpactStore({
36
+ ...opts,
37
+ ...(configForRoot === undefined ? {} : { config: configForRoot(key) }),
38
+ ...(coverageForRoot === undefined ? {} : { coverage: coverageForRoot(key) }),
39
+ reticleRoot: key,
40
+ });
35
41
  stores.set(key, found);
36
42
  }
37
43
  return found;
@@ -50,11 +56,15 @@ export function initImpact(opts) {
50
56
  return storeFor(undefined);
51
57
  if (defaultRoot === undefined) {
52
58
  defaultRoot = opts.reticleRoot;
53
- const { projectName, now, config } = opts;
59
+ const { projectName, now, config, configForRoot, coverageForRoot, globalRoot, sdkVersion } = opts;
54
60
  storeOpts = {
55
61
  ...(projectName === undefined ? {} : { projectName }),
56
62
  ...(now === undefined ? {} : { now }),
63
+ ...(globalRoot === undefined ? {} : { globalRoot }),
64
+ ...(sdkVersion === undefined ? {} : { sdkVersion }),
57
65
  ...(config === undefined ? {} : { config }),
66
+ ...(configForRoot === undefined ? {} : { configForRoot }),
67
+ ...(coverageForRoot === undefined ? {} : { coverageForRoot }),
58
68
  };
59
69
  }
60
70
  return storeFor(opts.reticleRoot);
@@ -1,5 +1,6 @@
1
1
  import { type OfferSource } from '../cloud/harness-offer.js';
2
2
  import type { ConfigSource } from '../cloud/harness-config.js';
3
+ import { type NoticesSource } from '../cloud/hud-notices-source.js';
3
4
  import { type ImpactCounts, type ImpactDefect, type AccountState, type ImpactScope, type ImpactSnapshot } from '@reticlehq/core';
4
5
  /** Read a scope, tolerating absence and corruption: a broken file starts over rather than throwing. */
5
6
  export declare function readScope(path: string, now: number): ImpactScope;
@@ -23,6 +24,15 @@ export interface ImpactFoldMeta {
23
24
  }
24
25
  /** Fold one delta into a scope: totals, today's bucket, records and streak. Pure. */
25
26
  export declare function applyDelta(scope: ImpactScope, delta: Partial<ImpactCounts>, now: number, meta?: ImpactFoldMeta): ImpactScope;
27
+ /**
28
+ * All time, never smaller than the project being looked at.
29
+ *
30
+ * The two scopes live in different files: the project's in its `.reticle`, the machine's in the
31
+ * home directory. A project whose history began before this machine's ledger (an upgrade, a new
32
+ * laptop, a moved checkout) showed more under "This project" than under "All time", which no reader
33
+ * can believe. The machine has done at least what any one of its projects has.
34
+ */
35
+ export declare function atLeastProject(global: ImpactScope, project: ImpactScope): ImpactScope;
26
36
  /**
27
37
  * The live impact record for one project.
28
38
  *
@@ -42,6 +52,12 @@ export declare class ImpactStore {
42
52
  /** Where the workspace stands with the free harness offer. Injected so no test touches a network. */
43
53
  offer?: OfferSource;
44
54
  config?: ConfigSource;
55
+ /** The HUD rail's notices. Defaults to the daemon's cached copy of the published file. */
56
+ notices?: NoticesSource;
57
+ /** This build's version, for notices that need a newer SDK. Passed in: the store reads no package. */
58
+ sdkVersion?: string;
59
+ /** Reticle Coverage per level, as percentages. Injected: the ledger lives in `features/exhaust`. */
60
+ coverage?: () => Record<string, number> | undefined;
45
61
  });
46
62
  /** Notified after every fold, so the HUD can be pushed a fresh snapshot. */
47
63
  onChange(fn: () => void): void;
@@ -1,23 +1,25 @@
1
1
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { describeSync, overallStatus, readSyncSummary } from '../project/sync-status.js';
2
3
  import { dirname, join } from 'node:path';
3
4
  import { homedir } from 'node:os';
4
5
  import { readAccountState } from '../cloud/account-state.js';
5
6
  import { harnessOfferSource } from '../cloud/harness-offer.js';
7
+ import { hudNoticesSource } from '../cloud/hud-notices-source.js';
8
+ import { selectNotices } from '@reticlehq/core/hud';
6
9
  import { ReticleDir, IMPACT_DAILY_BUCKETS, IMPACT_DEFECT_LIMIT, IMPACT_SCHEMA_VERSION, ImpactScopeSchema, addImpactCounts, emptyImpactCounts, emptyImpactRecords, estimateImpactSavings, } from '@reticlehq/core';
7
10
  /**
8
11
  * Where Reticle keeps the record of what it has done for you.
9
12
  *
10
- * Two scopes, two files, both local and never uploaded: the project's own `.reticle/impact.json`,
11
- * and a machine-wide `~/.reticle/impact.json` that answers "what has Reticle done for me overall"
12
- * across every app you have instrumented.
13
+ * Two scopes, two files: the project's own `.reticle/impact.json`, and a machine-wide
14
+ * `~/.reticle/impact.json` that answers "what has Reticle done for me overall" across every app you
15
+ * have instrumented.
13
16
  *
14
- * This is NOT telemetry. Telemetry answers our questions about the product and leaves the machine;
15
- * this answers the user's question about their own work and never does.
17
+ * This is NOT telemetry. Telemetry answers our questions about the product; this answers the user's
18
+ * question about their own work. A linked project's record syncs to that user's own dashboard (the
19
+ * sync cycle's `impact` kind); the machine-wide record never leaves the machine.
16
20
  */
17
21
  /** Writes are debounced: a verification loop is 50-200 calls, and each one is a counter bump. */
18
22
  const WRITE_DEBOUNCE_MS = 800;
19
- /** The project's cloud binding, written by `reticle link`. Absent for an unlinked project. */
20
- const CLOUD_LINK_FILE = 'cloud.json';
21
23
  /**
22
24
  * `reticleRoot` is the project's own `.reticle` directory (what every other store here is handed);
23
25
  * the global scope lives beside the daemon's own state in `~/.reticle`.
@@ -42,7 +44,7 @@ function impactPaths(reticleRoot, globalRoot) {
42
44
  */
43
45
  function readDashboardUrl(reticleRoot) {
44
46
  try {
45
- const raw = JSON.parse(readFileSync(join(reticleRoot, CLOUD_LINK_FILE), 'utf8'));
47
+ const raw = JSON.parse(readFileSync(join(reticleRoot, ReticleDir.CLOUD_LINK_FILE), 'utf8'));
46
48
  if ('object' !== typeof raw || null === raw)
47
49
  return undefined;
48
50
  const value = raw['dashboardUrl'];
@@ -92,6 +94,12 @@ function writeScope(path, scope) {
92
94
  // memory and the next write attempt carries them.
93
95
  }
94
96
  }
97
+ /** The daemon's cached copy of the HUD notices file, beside the machine-wide impact record. */
98
+ const HUD_NOTICES_CACHE_FILE = 'hud-notices.json';
99
+ /** How long a sync status is reused before it is read again. */
100
+ const SYNC_STATUS_EVERY_MS = 10_000;
101
+ /** When the caller did not say which build this is: matches only notices with no `minSdk`. */
102
+ const UNKNOWN_SDK_VERSION = '0.0.0';
95
103
  /** YYYY-MM-DD in local time - the day boundary a person recognises, not UTC's. */
96
104
  export function isoDay(now) {
97
105
  const d = new Date(now);
@@ -105,33 +113,55 @@ function isNextDay(a, b) {
105
113
  const next = new Date(`${b}T00:00:00`);
106
114
  return 1 === Math.round((next.getTime() - prev.getTime()) / 86_400_000);
107
115
  }
116
+ /**
117
+ * Consecutive days ending at the newest recorded day, read from the days themselves.
118
+ *
119
+ * It was a counter bumped when a fold opened a new day and compared only with the bucket before it,
120
+ * so one stray day in between (a late flush from another daemon, a writer with a frozen clock) reset
121
+ * it to 1. Counting back from the newest date means no out-of-order write can shorten it.
122
+ */
123
+ function streakEndingAtLatest(sortedDates) {
124
+ let streak = 0;
125
+ let later;
126
+ for (let i = sortedDates.length - 1; i >= 0; i -= 1) {
127
+ const date = sortedDates[i];
128
+ if (date === undefined)
129
+ break;
130
+ if (later !== undefined && !isNextDay(date, later))
131
+ break;
132
+ streak += 1;
133
+ later = date;
134
+ }
135
+ return streak;
136
+ }
108
137
  /** Fold one delta into a scope: totals, today's bucket, records and streak. Pure. */
109
138
  export function applyDelta(scope, delta, now, meta = {}) {
110
139
  const today = isoDay(now);
111
140
  const counts = addImpactCounts(scope.counts, delta);
112
- const days = scope.days.slice();
113
- const last = days[days.length - 1];
114
- if (last !== undefined && last.date === today) {
115
- days[days.length - 1] = { date: today, counts: addImpactCounts(last.counts, delta) };
116
- }
117
- else {
118
- days.push({ date: today, counts: addImpactCounts(emptyImpactCounts(), delta) });
141
+ // One bucket per date, wherever the delta's date falls. The machine-wide record has many writers
142
+ // and a late flush carries an older date, so "the last bucket is today" is not something a fold
143
+ // may assume — appending on that assumption duplicated dates and put them out of order.
144
+ const byDate = new Map();
145
+ for (const day of scope.days) {
146
+ byDate.set(day.date, addImpactCounts(byDate.get(day.date) ?? emptyImpactCounts(), day.counts));
119
147
  }
148
+ const todayCounts = addImpactCounts(byDate.get(today) ?? emptyImpactCounts(), delta);
149
+ byDate.set(today, todayCounts);
150
+ const dates = [...byDate.keys()].sort();
151
+ const days = dates.map((date) => ({ date, counts: byDate.get(date) ?? emptyImpactCounts() }));
120
152
  while (days.length > IMPACT_DAILY_BUCKETS)
121
153
  days.shift();
122
- const todayCounts = days[days.length - 1]?.counts ?? emptyImpactCounts();
123
- const previousDay = days[days.length - 2];
124
154
  const records = { ...scope.records };
125
155
  records.bestVerdictDay = Math.max(records.bestVerdictDay, todayCounts.verdicts);
126
156
  records.bestDefectDay = Math.max(records.bestDefectDay, todayCounts.failed);
127
- if (last === undefined || last.date !== today) {
128
- // A new day joins the streak only if it follows yesterday; otherwise it starts a new one.
129
- records.streakDays =
130
- previousDay !== undefined && isNextDay(previousDay.date, today) ? records.streakDays + 1 : 1;
131
- }
132
- else if (0 === records.streakDays) {
133
- records.streakDays = 1;
134
- }
157
+ // The window keeps IMPACT_DAILY_BUCKETS days, so a streak longer than it cannot be read off the
158
+ // dates alone: when the whole window is one unbroken run, it continues the streak already held.
159
+ const inWindow = streakEndingAtLatest(dates);
160
+ const unbroken = inWindow === dates.length && scope.days.length >= IMPACT_DAILY_BUCKETS;
161
+ const newDay = !scope.days.some((day) => day.date === today);
162
+ records.streakDays = unbroken
163
+ ? Math.max(inWindow, scope.records.streakDays + (newDay ? 1 : 0))
164
+ : inWindow;
135
165
  records.bestStreakDays = Math.max(records.bestStreakDays, records.streakDays);
136
166
  records.longestRunMs = Math.max(records.longestRunMs, meta.runMs ?? 0);
137
167
  /*
@@ -150,6 +180,41 @@ export function applyDelta(scope, delta, now, meta = {}) {
150
180
  defects,
151
181
  };
152
182
  }
183
+ /** The larger of two counters, key by key. */
184
+ function maxEach(a, b) {
185
+ const out = { ...a };
186
+ for (const key of Object.keys(b))
187
+ out[key] = Math.max(a[key] ?? 0, b[key] ?? 0);
188
+ return out;
189
+ }
190
+ /**
191
+ * All time, never smaller than the project being looked at.
192
+ *
193
+ * The two scopes live in different files: the project's in its `.reticle`, the machine's in the
194
+ * home directory. A project whose history began before this machine's ledger (an upgrade, a new
195
+ * laptop, a moved checkout) showed more under "This project" than under "All time", which no reader
196
+ * can believe. The machine has done at least what any one of its projects has.
197
+ */
198
+ export function atLeastProject(global, project) {
199
+ // The earlier of the two starts; zero is "never recorded", not the epoch.
200
+ const starts = [global.since, project.since].filter((t) => 0 < t);
201
+ return {
202
+ ...global,
203
+ counts: maxEach(global.counts, project.counts),
204
+ records: maxEach(global.records, project.records),
205
+ savings: {
206
+ tokens: {
207
+ ...global.savings.tokens,
208
+ value: Math.max(global.savings.tokens.value, project.savings.tokens.value),
209
+ },
210
+ minutes: {
211
+ ...global.savings.minutes,
212
+ value: Math.max(global.savings.minutes.value, project.savings.minutes.value),
213
+ },
214
+ },
215
+ since: 0 === starts.length ? 0 : Math.min(...starts),
216
+ };
217
+ }
153
218
  /**
154
219
  * The live impact record for one project.
155
220
  *
@@ -158,6 +223,9 @@ export function applyDelta(scope, delta, now, meta = {}) {
158
223
  */
159
224
  export class ImpactStore {
160
225
  #paths;
226
+ #root;
227
+ /** The sync status, re-read at most every SYNC_STATUS_EVERY_MS: it reads every run file. */
228
+ #sync;
161
229
  #now;
162
230
  #projectName;
163
231
  #dashboardUrl;
@@ -178,7 +246,11 @@ export class ImpactStore {
178
246
  /** Where this workspace stands with the free harness offer. Cached; see harness-offer.ts. */
179
247
  #offer;
180
248
  #config;
249
+ #notices;
250
+ #sdkVersion;
251
+ #coverage;
181
252
  constructor(opts) {
253
+ this.#coverage = opts.coverage;
182
254
  this.#paths = impactPaths(opts.reticleRoot, opts.globalRoot ?? homedir());
183
255
  // Resolved per snapshot, not cached: a user who runs `reticle login` in another terminal must
184
256
  // see the HUD change without restarting the daemon that is watching their app.
@@ -186,9 +258,16 @@ export class ImpactStore {
186
258
  this.#now = opts.now ?? (() => Date.now());
187
259
  this.#projectName = opts.projectName;
188
260
  this.#dashboardUrl = readDashboardUrl(opts.reticleRoot);
261
+ this.#root = opts.reticleRoot;
189
262
  // The claim happens in the console, so the link this project was linked to IS the claim link —
190
263
  // built here rather than in the HUD, which has no way to know where this project points.
191
264
  this.#offer = opts.offer ?? harnessOfferSource(process.env, () => this.#dashboardUrl);
265
+ this.#notices =
266
+ opts.notices ??
267
+ hudNoticesSource({
268
+ cacheFile: join(opts.globalRoot ?? homedir(), ReticleDir.ROOT, HUD_NOTICES_CACHE_FILE),
269
+ });
270
+ this.#sdkVersion = opts.sdkVersion ?? UNKNOWN_SDK_VERSION;
192
271
  // No default: the loader lives in `features/harness` and this file may not reach for it.
193
272
  // A store built without one simply reports no harness config, which the HUD renders as no row.
194
273
  this.#config = opts.config ?? { read: () => undefined };
@@ -196,6 +275,23 @@ export class ImpactStore {
196
275
  this.#project = readScope(this.#paths.project, now);
197
276
  this.#global = readScope(this.#paths.global, now);
198
277
  }
278
+ #syncStatus() {
279
+ const now = this.#now();
280
+ if (this.#sync !== undefined && now - this.#sync.at < SYNC_STATUS_EVERY_MS)
281
+ return this.#sync.value;
282
+ const summary = readSyncSummary(this.#root, true);
283
+ const value = {
284
+ status: overallStatus(summary),
285
+ runs: summary.runs,
286
+ onPlatform: summary.onPlatform,
287
+ pending: summary.pending,
288
+ refused: summary.refused.length + summary.refusedMore,
289
+ ...(summary.lastPushAt === undefined ? {} : { lastPushAt: summary.lastPushAt }),
290
+ said: describeSync(summary, now),
291
+ };
292
+ this.#sync = { at: now, value };
293
+ return value;
294
+ }
199
295
  /** Notified after every fold, so the HUD can be pushed a fresh snapshot. */
200
296
  onChange(fn) {
201
297
  this.#onChange = fn;
@@ -214,10 +310,13 @@ export class ImpactStore {
214
310
  const snap = {
215
311
  schemaVersion: IMPACT_SCHEMA_VERSION,
216
312
  project: this.#project,
217
- global: this.#global,
313
+ global: atLeastProject(this.#global, this.#project),
218
314
  };
219
315
  if (this.#projectName !== undefined)
220
316
  snap.projectName = this.#projectName;
317
+ // Re-read, like the account: `reticle link` runs in another terminal while this store lives as
318
+ // long as the daemon, so a construction-time read said "Not linked" until a restart.
319
+ this.#dashboardUrl = readDashboardUrl(this.#root);
221
320
  if (this.#dashboardUrl !== undefined)
222
321
  snap.dashboardUrl = this.#dashboardUrl;
223
322
  snap.account = this.#account();
@@ -236,6 +335,23 @@ export class ImpactStore {
236
335
  const cfg = this.#config.read();
237
336
  if (cfg !== undefined)
238
337
  snap.harnessConfig = cfg;
338
+ // Chosen here, where the account and the entitlement are both known. Absent when nothing applies,
339
+ // so the HUD falls back to the slides bundled with the SDK.
340
+ const notices = selectNotices(this.#notices.read(), {
341
+ signedIn: snap.account?.signedIn,
342
+ entitled: cfg?.harnessEntitled,
343
+ sdkVersion: this.#sdkVersion,
344
+ now: this.#now(),
345
+ });
346
+ if (0 < notices.length)
347
+ snap.notices = notices;
348
+ const coverage = this.#coverage?.();
349
+ if (coverage !== undefined)
350
+ snap.coverage = coverage;
351
+ // Only for a linked project: an unlinked one has nothing on the platform, and the HUD already
352
+ // offers the way to link it.
353
+ if (this.#dashboardUrl !== undefined)
354
+ snap.sync = this.#syncStatus();
239
355
  return snap;
240
356
  }
241
357
  /** Write now (process exit, tests). */
@@ -252,7 +252,6 @@ export declare const IntentShardSchema: z.ZodObject<{
252
252
  }>>;
253
253
  }, "strip", z.ZodTypeAny, {
254
254
  version: 1;
255
- subject: string;
256
255
  intents: Record<string, {
257
256
  status: "proved" | "proposed" | "agreed" | "stale";
258
257
  state: "proved" | "declared" | "bound";
@@ -279,9 +278,9 @@ export declare const IntentShardSchema: z.ZodObject<{
279
278
  statement: string;
280
279
  }[] | undefined;
281
280
  }>;
281
+ subject: string;
282
282
  }, {
283
283
  version: 1;
284
- subject: string;
285
284
  intents: Record<string, {
286
285
  status: "proved" | "proposed" | "agreed" | "stale";
287
286
  state: "proved" | "declared" | "bound";
@@ -308,6 +307,7 @@ export declare const IntentShardSchema: z.ZodObject<{
308
307
  statement: string;
309
308
  }[] | undefined;
310
309
  }>;
310
+ subject: string;
311
311
  }>;
312
312
  export type IntentShard = z.infer<typeof IntentShardSchema>;
313
313
  export declare const IntentIndexSchema: z.ZodObject<{