@reticlehq/server 2.12.0 → 2.13.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 (386) hide show
  1. package/SKILL.md +67 -109
  2. package/dist/bridge/auth-failure-reason.d.ts +6 -2
  3. package/dist/bridge/auth-failure-reason.js +15 -8
  4. package/dist/bridge/bridge.d.ts +18 -1
  5. package/dist/bridge/bridge.js +247 -164
  6. package/dist/bridge/pairing-token.d.ts +5 -0
  7. package/dist/bridge/pairing-token.js +28 -0
  8. package/dist/bridge/protocol-skew.d.ts +37 -0
  9. package/dist/bridge/protocol-skew.js +45 -0
  10. package/dist/capsule/capsule-store.d.ts +6 -6
  11. package/dist/capsule/causal-summary.js +7 -2
  12. package/dist/cli/cli-doctor.js +31 -1
  13. package/dist/cli/cli-launch.d.ts +11 -1
  14. package/dist/cli/cli-launch.js +59 -10
  15. package/dist/cli/cli-parse.d.ts +15 -1
  16. package/dist/cli/cli-parse.js +212 -8
  17. package/dist/cli/cli-verify.d.ts +1 -0
  18. package/dist/cli/cli-verify.js +3 -2
  19. package/dist/cli/cloud-cli.js +428 -259
  20. package/dist/cli/cloud-keystore.d.ts +37 -0
  21. package/dist/cli/cloud-keystore.js +97 -0
  22. package/dist/cli/cloud-kit.d.ts +37 -0
  23. package/dist/cli/cloud-kit.js +121 -0
  24. package/dist/cli/cloud-login.d.ts +17 -0
  25. package/dist/cli/cloud-login.js +183 -0
  26. package/dist/cli/cloud-session.d.ts +48 -0
  27. package/dist/cli/cloud-session.js +53 -0
  28. package/dist/cli/config-discovery.js +1 -0
  29. package/dist/cli/daemon-start-options.d.ts +32 -0
  30. package/dist/cli/daemon-start-options.js +38 -0
  31. package/dist/cli/device-flow.d.ts +76 -0
  32. package/dist/cli/device-flow.js +56 -0
  33. package/dist/cli/doctor-rows.d.ts +9 -0
  34. package/dist/cli/doctor-rows.js +9 -0
  35. package/dist/cli/drive-attach.d.ts +13 -0
  36. package/dist/cli/drive-attach.js +18 -0
  37. package/dist/cli/drive-command.js +7 -3
  38. package/dist/cli/mcp-command.js +51 -20
  39. package/dist/cli/project-name.d.ts +30 -0
  40. package/dist/cli/project-name.js +44 -0
  41. package/dist/cli/status-next-action.d.ts +14 -0
  42. package/dist/cli/status-next-action.js +4 -4
  43. package/dist/cli.d.ts +1 -0
  44. package/dist/cli.js +134 -80
  45. package/dist/cloud/cloud-config.d.ts +22 -1
  46. package/dist/cloud/cloud-config.js +64 -5
  47. package/dist/cloud/cloud-sync.d.ts +5 -5
  48. package/dist/cloud/cloud-sync.js +33 -2
  49. package/dist/cloud/sync-cycle.d.ts +110 -0
  50. package/dist/cloud/sync-cycle.js +270 -0
  51. package/dist/cloud/sync-daemon.d.ts +69 -0
  52. package/dist/cloud/sync-daemon.js +260 -0
  53. package/dist/cloud/sync-disk.d.ts +9 -0
  54. package/dist/cloud/sync-disk.js +191 -0
  55. package/dist/cloud/sync-hash.d.ts +2 -0
  56. package/dist/cloud/sync-hash.js +35 -0
  57. package/dist/crawl/crawl-tools.js +5 -3
  58. package/dist/crawl/crawl.d.ts +3 -0
  59. package/dist/crawl/crawl.js +5 -0
  60. package/dist/daemon/daemon-resolve.d.ts +43 -0
  61. package/dist/daemon/daemon-resolve.js +69 -0
  62. package/dist/daemon/dev-servers.d.ts +15 -0
  63. package/dist/daemon/dev-servers.js +68 -0
  64. package/dist/daemon/wake-decision.d.ts +32 -0
  65. package/dist/daemon/wake-decision.js +38 -0
  66. package/dist/domain/domain-tools.js +2 -2
  67. package/dist/events/contradictions.d.ts +18 -0
  68. package/dist/events/contradictions.js +134 -21
  69. package/dist/events/declared.d.ts +7 -4
  70. package/dist/events/declared.js +15 -6
  71. package/dist/events/echo-mismatch.js +64 -14
  72. package/dist/events/event-filters.d.ts +8 -0
  73. package/dist/events/event-filters.js +19 -2
  74. package/dist/events/predicate-eval.js +53 -5
  75. package/dist/events/predicate-precheck.d.ts +8 -0
  76. package/dist/events/predicate-precheck.js +41 -0
  77. package/dist/events/predicate-route.d.ts +14 -0
  78. package/dist/events/predicate-route.js +36 -8
  79. package/dist/events/predicate.d.ts +5 -0
  80. package/dist/events/predicate.js +37 -5
  81. package/dist/events/split-text-miss.d.ts +30 -0
  82. package/dist/events/split-text-miss.js +44 -0
  83. package/dist/flows/flake.d.ts +6 -6
  84. package/dist/flows/flow-memory-consult.d.ts +55 -0
  85. package/dist/flows/flow-memory-consult.js +38 -0
  86. package/dist/flows/flow-replay-run.d.ts +18 -7
  87. package/dist/flows/flow-replay-run.js +239 -22
  88. package/dist/flows/flow-replay.js +11 -2
  89. package/dist/flows/flow-step-runners.d.ts +8 -0
  90. package/dist/flows/flow-step-runners.js +8 -2
  91. package/dist/flows/flow-store-for-session.d.ts +31 -0
  92. package/dist/flows/flow-store-for-session.js +33 -0
  93. package/dist/flows/flow-tools.js +58 -32
  94. package/dist/flows/flows.d.ts +8 -0
  95. package/dist/flows/flows.js +468 -2
  96. package/dist/flows/heal-run.js +5 -2
  97. package/dist/flows/predicate-to-expect.d.ts +17 -12
  98. package/dist/flows/predicate-to-expect.js +30 -12
  99. package/dist/flows/recordings.d.ts +13 -1
  100. package/dist/flows/recordings.js +17 -2
  101. package/dist/flows/replay.d.ts +22 -2
  102. package/dist/flows/replay.js +28 -2
  103. package/dist/flows/verify-change-tools.js +5 -2
  104. package/dist/honesty/blind-spots.d.ts +55 -2
  105. package/dist/honesty/blind-spots.js +63 -3
  106. package/dist/honesty/honesty.d.ts +24 -0
  107. package/dist/honesty/honesty.js +25 -1
  108. package/dist/honesty/instrumentation-gaps.d.ts +9 -0
  109. package/dist/honesty/instrumentation-gaps.js +29 -2
  110. package/dist/honesty/verified.d.ts +2 -1
  111. package/dist/honesty/verified.js +16 -1
  112. package/dist/http-server.js +12 -0
  113. package/dist/impact/impact-recorder.d.ts +31 -8
  114. package/dist/impact/impact-recorder.js +104 -21
  115. package/dist/impact/impact-store.d.ts +15 -1
  116. package/dist/impact/impact-store.js +43 -2
  117. package/dist/index.js +113 -7
  118. package/dist/init/agent-rules.d.ts +13 -6
  119. package/dist/init/agent-rules.js +23 -12
  120. package/dist/init/astro-patch.d.ts +16 -0
  121. package/dist/init/astro-patch.js +28 -0
  122. package/dist/init/capabilities.d.ts +4 -1
  123. package/dist/init/capabilities.js +22 -2
  124. package/dist/init/confirm.d.ts +31 -7
  125. package/dist/init/confirm.js +48 -8
  126. package/dist/init/cra.d.ts +20 -4
  127. package/dist/init/cra.js +57 -17
  128. package/dist/init/csp-check.js +17 -2
  129. package/dist/init/csp-doctor.d.ts +8 -1
  130. package/dist/init/csp-doctor.js +8 -3
  131. package/dist/init/dev-script.d.ts +3 -1
  132. package/dist/init/dev-script.js +8 -5
  133. package/dist/init/format-generated.d.ts +18 -0
  134. package/dist/init/format-generated.js +39 -0
  135. package/dist/init/install-hint.d.ts +9 -0
  136. package/dist/init/install-hint.js +29 -0
  137. package/dist/init/mcp-clients.d.ts +7 -0
  138. package/dist/init/mcp-clients.js +7 -0
  139. package/dist/init/mcp.d.ts +1 -1
  140. package/dist/init/mcp.js +1 -1
  141. package/dist/init/node-io.js +11 -1
  142. package/dist/init/plan-framework.js +81 -45
  143. package/dist/init/plan.d.ts +20 -7
  144. package/dist/init/plan.js +20 -45
  145. package/dist/init/preflight.d.ts +28 -0
  146. package/dist/init/preflight.js +38 -0
  147. package/dist/init/run.d.ts +32 -9
  148. package/dist/init/run.js +141 -119
  149. package/dist/init/slash-command.d.ts +1 -1
  150. package/dist/init/snippets.js +41 -25
  151. package/dist/init/vite-config.d.ts +1 -1
  152. package/dist/init/vite-config.js +39 -25
  153. package/dist/init/workspace-apps.d.ts +17 -10
  154. package/dist/init/workspace-apps.js +110 -0
  155. package/dist/input/network-mock-tools.js +3 -6
  156. package/dist/input/real-input.js +25 -5
  157. package/dist/input/scroll-tools.js +4 -6
  158. package/dist/input/viewport-tools.js +5 -4
  159. package/dist/intent/inline-intent.d.ts +10 -19
  160. package/dist/intent/inline-intent.js +91 -1
  161. package/dist/intent/intent-shard-store.d.ts +72 -0
  162. package/dist/intent/intent-shard-store.js +203 -0
  163. package/dist/intent/intent-shard.d.ts +411 -0
  164. package/dist/intent/intent-shard.js +176 -0
  165. package/dist/intent/intent-store.d.ts +12 -0
  166. package/dist/intent/intent-store.js +21 -0
  167. package/dist/intent/intent-subject.d.ts +45 -0
  168. package/dist/intent/intent-subject.js +177 -0
  169. package/dist/intent/intent-tools.js +65 -2
  170. package/dist/intent/open-intents.d.ts +21 -0
  171. package/dist/intent/open-intents.js +15 -0
  172. package/dist/license/license-env.d.ts +29 -0
  173. package/dist/license/license-env.js +148 -0
  174. package/dist/loopback-agent.d.ts +31 -0
  175. package/dist/loopback-agent.js +57 -0
  176. package/dist/mcp/attach-memory.d.ts +18 -2
  177. package/dist/mcp/attach-memory.js +50 -7
  178. package/dist/mcp/client-identity.d.ts +31 -0
  179. package/dist/mcp/client-identity.js +51 -0
  180. package/dist/mcp/mcp-post-transport.d.ts +26 -0
  181. package/dist/mcp/mcp-post-transport.js +109 -0
  182. package/dist/mcp/mcp-proxy.d.ts +2 -1
  183. package/dist/mcp/mcp-proxy.js +52 -62
  184. package/dist/mcp/mcp.js +7 -1
  185. package/dist/mcp/proxy-backoff.d.ts +11 -0
  186. package/dist/mcp/proxy-backoff.js +13 -0
  187. package/dist/mcp/proxy-daemon-probe.d.ts +1 -0
  188. package/dist/mcp/proxy-daemon-probe.js +7 -1
  189. package/dist/mcp/proxy-handshake.d.ts +23 -0
  190. package/dist/mcp/proxy-handshake.js +31 -0
  191. package/dist/mcp/tool-catalog-cache.d.ts +8 -0
  192. package/dist/mcp/tool-catalog-cache.js +12 -0
  193. package/dist/memory/memory-tools.d.ts +2 -0
  194. package/dist/memory/memory-tools.js +82 -0
  195. package/dist/memory/project-memory.d.ts +34 -0
  196. package/dist/memory/project-memory.js +96 -0
  197. package/dist/oracles/propose-consequences.js +12 -3
  198. package/dist/pool/browser-pool.d.ts +63 -4
  199. package/dist/pool/browser-pool.js +113 -4
  200. package/dist/pool/playwright-launcher.js +9 -0
  201. package/dist/project/project-tools.js +3 -3
  202. package/dist/project/reticle-dir.d.ts +15 -2
  203. package/dist/project/reticle-dir.js +21 -4
  204. package/dist/runs/run-store.d.ts +10 -0
  205. package/dist/runs/run-store.js +10 -0
  206. package/dist/runs/verification-sync.d.ts +0 -11
  207. package/dist/runs/verification-sync.js +22 -1
  208. package/dist/runs/verify-server.d.ts +8 -1
  209. package/dist/runs/verify-server.js +14 -2
  210. package/dist/session/command-timeout.d.ts +5 -2
  211. package/dist/session/command-timeout.js +5 -4
  212. package/dist/session/lease-visibility.d.ts +67 -0
  213. package/dist/session/lease-visibility.js +76 -0
  214. package/dist/session/no-session-diagnosis.d.ts +23 -0
  215. package/dist/session/no-session-diagnosis.js +58 -17
  216. package/dist/session/no-session-next-action.d.ts +7 -0
  217. package/dist/session/no-session-next-action.js +42 -3
  218. package/dist/session/no-session-watch.d.ts +10 -6
  219. package/dist/session/no-session-watch.js +51 -19
  220. package/dist/session/pending-commands.d.ts +11 -1
  221. package/dist/session/pending-commands.js +12 -2
  222. package/dist/session/resolve-within.d.ts +34 -0
  223. package/dist/session/resolve-within.js +53 -0
  224. package/dist/session/session-info.d.ts +18 -0
  225. package/dist/session/session-info.js +11 -0
  226. package/dist/session/session-manager.d.ts +14 -0
  227. package/dist/session/session-manager.js +61 -3
  228. package/dist/session/session-mirror.d.ts +16 -0
  229. package/dist/session/session-mirror.js +22 -0
  230. package/dist/session/session-recommendation.d.ts +19 -5
  231. package/dist/session/session-recommendation.js +25 -9
  232. package/dist/session/session-successor.js +36 -1
  233. package/dist/session/session-tools.js +2 -2
  234. package/dist/session/session.d.ts +22 -2
  235. package/dist/session/session.js +87 -7
  236. package/dist/session/wall-clock.d.ts +12 -0
  237. package/dist/session/wall-clock.js +14 -0
  238. package/dist/setup/agent-configs.d.ts +96 -0
  239. package/dist/setup/agent-configs.js +333 -0
  240. package/dist/setup/agent-io.d.ts +9 -0
  241. package/dist/setup/agent-io.js +16 -0
  242. package/dist/setup/agent-writer.d.ts +51 -0
  243. package/dist/setup/agent-writer.js +115 -0
  244. package/dist/setup/approval-migration.d.ts +47 -0
  245. package/dist/setup/approval-migration.js +68 -0
  246. package/dist/setup/auto-approve.d.ts +85 -0
  247. package/dist/setup/auto-approve.js +150 -0
  248. package/dist/setup/bridge-port.d.ts +15 -0
  249. package/dist/setup/bridge-port.js +24 -0
  250. package/dist/setup/desktop-shape.d.ts +57 -0
  251. package/dist/setup/desktop-shape.js +63 -0
  252. package/dist/setup/dev-server-wait.d.ts +60 -0
  253. package/dist/setup/dev-server-wait.js +65 -0
  254. package/dist/setup/drive-agent.d.ts +71 -0
  255. package/dist/setup/drive-agent.js +171 -0
  256. package/dist/setup/drive-plan.d.ts +68 -0
  257. package/dist/setup/drive-plan.js +130 -0
  258. package/dist/setup/ensure-daemon.d.ts +29 -0
  259. package/dist/setup/ensure-daemon.js +58 -0
  260. package/dist/setup/init-runtime.d.ts +47 -0
  261. package/dist/setup/init-runtime.js +160 -0
  262. package/dist/setup/interrupt.d.ts +25 -0
  263. package/dist/setup/interrupt.js +40 -0
  264. package/dist/setup/license-key.d.ts +39 -0
  265. package/dist/setup/license-key.js +72 -0
  266. package/dist/setup/listeners.d.ts +37 -0
  267. package/dist/setup/listeners.js +85 -0
  268. package/dist/setup/node-effects.d.ts +60 -0
  269. package/dist/setup/node-effects.js +210 -0
  270. package/dist/setup/page-probe.d.ts +39 -0
  271. package/dist/setup/page-probe.js +49 -0
  272. package/dist/setup/relaunch.d.ts +36 -0
  273. package/dist/setup/relaunch.js +57 -0
  274. package/dist/setup/remaining-steps.d.ts +19 -0
  275. package/dist/setup/remaining-steps.js +31 -0
  276. package/dist/setup/run-setup.d.ts +99 -0
  277. package/dist/setup/run-setup.js +187 -0
  278. package/dist/setup/session-pick.d.ts +47 -0
  279. package/dist/setup/session-pick.js +51 -0
  280. package/dist/setup/setup-command.d.ts +45 -0
  281. package/dist/setup/setup-command.js +199 -0
  282. package/dist/setup/setup-options.d.ts +74 -0
  283. package/dist/setup/setup-options.js +67 -0
  284. package/dist/setup/transcripts.d.ts +16 -0
  285. package/dist/setup/transcripts.js +43 -0
  286. package/dist/status-payload.d.ts +14 -1
  287. package/dist/status-payload.js +23 -1
  288. package/dist/telemetry/daemon-telemetry.d.ts +8 -1
  289. package/dist/telemetry/daemon-telemetry.js +52 -5
  290. package/dist/telemetry/error-class.js +1 -1
  291. package/dist/telemetry/feedback-context.d.ts +9 -1
  292. package/dist/telemetry/feedback-context.js +60 -29
  293. package/dist/telemetry/feedback-local.d.ts +15 -0
  294. package/dist/telemetry/feedback-local.js +56 -0
  295. package/dist/telemetry/feedback.d.ts +11 -0
  296. package/dist/telemetry/feedback.js +19 -6
  297. package/dist/telemetry/init-telemetry.d.ts +2 -0
  298. package/dist/telemetry/init-telemetry.js +2 -0
  299. package/dist/telemetry/install-source.d.ts +14 -0
  300. package/dist/telemetry/install-source.js +30 -0
  301. package/dist/telemetry/license-activation.d.ts +7 -1
  302. package/dist/telemetry/license-activation.js +9 -5
  303. package/dist/telemetry/mcp-connection.js +7 -0
  304. package/dist/telemetry/project-profile.d.ts +12 -1
  305. package/dist/telemetry/project-profile.js +11 -6
  306. package/dist/telemetry/proxy-telemetry.d.ts +3 -0
  307. package/dist/telemetry/proxy-telemetry.js +21 -0
  308. package/dist/telemetry/session-metrics.d.ts +12 -0
  309. package/dist/telemetry/session-metrics.js +35 -1
  310. package/dist/telemetry/telemetry.js +17 -2
  311. package/dist/telemetry/tool-refused.d.ts +3 -1
  312. package/dist/telemetry/tool-refused.js +34 -2
  313. package/dist/tools/act-capsule.d.ts +10 -0
  314. package/dist/tools/act-capsule.js +8 -2
  315. package/dist/tools/act-danger.d.ts +2 -1
  316. package/dist/tools/act-danger.js +11 -3
  317. package/dist/tools/act-observation.d.ts +41 -0
  318. package/dist/tools/act-observation.js +36 -0
  319. package/dist/tools/act-preflight.d.ts +20 -0
  320. package/dist/tools/act-preflight.js +98 -0
  321. package/dist/tools/act-sequence-retry.d.ts +45 -0
  322. package/dist/tools/act-sequence-retry.js +58 -0
  323. package/dist/tools/act-sequence-tool.d.ts +13 -0
  324. package/dist/tools/act-sequence-tool.js +135 -0
  325. package/dist/tools/act-target.d.ts +14 -0
  326. package/dist/tools/act-target.js +47 -0
  327. package/dist/tools/act-tools.d.ts +21 -1
  328. package/dist/tools/act-tools.js +100 -213
  329. package/dist/tools/assert-verdict.js +22 -16
  330. package/dist/tools/browser-tools.js +6 -2
  331. package/dist/tools/dynamic-tools.js +24 -5
  332. package/dist/tools/error-recovery.d.ts +1 -0
  333. package/dist/tools/error-recovery.js +8 -0
  334. package/dist/tools/invoke-tool.js +59 -9
  335. package/dist/tools/lease-tools.d.ts +30 -4
  336. package/dist/tools/lease-tools.js +169 -5
  337. package/dist/tools/merged-name-redirect.d.ts +13 -0
  338. package/dist/tools/merged-name-redirect.js +22 -0
  339. package/dist/tools/numeric-bounds.d.ts +100 -0
  340. package/dist/tools/numeric-bounds.js +125 -0
  341. package/dist/tools/observe-tools.js +47 -40
  342. package/dist/tools/read-tools.js +28 -7
  343. package/dist/tools/real-input-attempt.d.ts +10 -0
  344. package/dist/tools/real-input-attempt.js +59 -1
  345. package/dist/tools/reconcile-tools.js +4 -4
  346. package/dist/tools/tool-kit.d.ts +12 -0
  347. package/dist/tools/tool-kit.js +6 -1
  348. package/dist/tools/tool-names.d.ts +2 -0
  349. package/dist/tools/tool-names.js +2 -0
  350. package/dist/tools/tools-helpers.d.ts +19 -0
  351. package/dist/tools/tools-helpers.js +46 -0
  352. package/dist/tools/tools.d.ts +2 -0
  353. package/dist/tools/tools.js +56 -8
  354. package/dist/update/reticle-deps.d.ts +9 -0
  355. package/dist/update/reticle-deps.js +27 -0
  356. package/dist/update/sdk-sync.d.ts +1 -2
  357. package/dist/update/sdk-sync.js +1 -21
  358. package/dist/update/update-nudge.d.ts +18 -0
  359. package/dist/update/update-nudge.js +20 -0
  360. package/dist/version/peer-announce.js +4 -2
  361. package/dist/version/sdk-fix.d.ts +47 -0
  362. package/dist/version/sdk-fix.js +127 -0
  363. package/dist/version/version-nudge.d.ts +14 -0
  364. package/dist/version/version-nudge.js +21 -0
  365. package/dist/version/version-skew.d.ts +38 -3
  366. package/dist/version/version-skew.js +83 -7
  367. package/dist/visual/visual-store.d.ts +6 -6
  368. package/dist/visual/visual-store.js +18 -16
  369. package/dist/visual/visual-tools.js +57 -12
  370. package/docs/cli/doctor.mdx +1 -0
  371. package/docs/cli/serve.mdx +4 -2
  372. package/docs/cli.mdx +7 -3
  373. package/docs/desktop.mdx +1 -1
  374. package/docs/docs.json +1 -1
  375. package/docs/getting-started.md +19 -3
  376. package/docs/index.mdx +9 -6
  377. package/docs/install-agentic.mdx +38 -12
  378. package/docs/install-manual.mdx +1 -1
  379. package/docs/packages.mdx +1 -1
  380. package/docs/skill-file.mdx +3 -3
  381. package/docs/telemetry-contract.md +108 -4
  382. package/docs/tools-act-sequence.mdx +16 -0
  383. package/docs/usage.md +1 -1
  384. package/package.json +4 -3
  385. package/dist/init/cursor.d.ts +0 -24
  386. package/dist/init/cursor.js +0 -80
@@ -1,43 +1,90 @@
1
- import { Verified } from '@reticlehq/core';
1
+ import { Verified, } from '@reticlehq/core';
2
2
  import { estimateTokens } from '../session/output-budget.js';
3
3
  import { ImpactStore } from './impact-store.js';
4
4
  /**
5
- * The daemon's single impact recorder.
5
+ * The daemon's impact recorders — ONE PER PROJECT ROOT.
6
6
  *
7
- * One store per daemon, created on the first call that knows the project's `.reticle` root. A
8
- * module singleton for the same reason the tool counters are: the dispatch chokepoint has to be
9
- * able to record without every tool threading a store through its arguments, and a second recorder
10
- * would silently split the user's own history in half.
7
+ * This was a single module-level store, and "the first root wins for the daemon's lifetime" was
8
+ * written down as if it were a design decision. It is only correct for a daemon that serves one
9
+ * project, and this daemon has never been that: `artifactRootFor` exists precisely because one
10
+ * daemon serves many, and nine modules already route their artifacts by the SESSION's project.
11
+ *
12
+ * Measured, not theorised: with a daemon rooted at repo A, two verdicts driven against app B landed
13
+ * in A's `impact.json` — B's own ledger never moved — and A's cloud link then carried them to a
14
+ * PRODUCTION dashboard belonging to a different account than B was linked to. The impact ledger is
15
+ * what the whole dashboard is computed from, so this was the one artifact whose misrouting was
16
+ * guaranteed to be visible and billed.
17
+ *
18
+ * Keyed by root path, so two projects driven through one daemon keep separate ledgers, and the
19
+ * daemon's own root remains the answer for anything that cannot name a project.
20
+ */
21
+ /** One store per `.reticle` root. */
22
+ const stores = new Map();
23
+ /** The daemon's own root — used when a call cannot say which project it was for. */
24
+ let defaultRoot;
25
+ /** Held so a per-root store is built with the same clock and project name as the default. */
26
+ let storeOpts = {};
27
+ /** Get-or-create the store for one root. */
28
+ function storeFor(root) {
29
+ const key = root ?? defaultRoot;
30
+ if (key === undefined || 0 === key.length)
31
+ return undefined;
32
+ let found = stores.get(key);
33
+ if (found === undefined) {
34
+ found = new ImpactStore({ ...storeOpts, reticleRoot: key });
35
+ stores.set(key, found);
36
+ }
37
+ return found;
38
+ }
39
+ /**
40
+ * Wire the DEFAULT root — the daemon's own. Idempotent; the first one wins.
41
+ *
42
+ * Still idempotent, and still first-wins, because the daemon's own root genuinely does not change
43
+ * while it runs. What changed is that this is no longer the ONLY root: a call that knows its
44
+ * project passes one, and this is the fallback for calls that do not.
11
45
  */
12
- let store;
13
- /** Wire the recorder to a project. Idempotent; the first root wins for the daemon's lifetime. */
14
46
  export function initImpact(opts) {
15
47
  // No root, no record. Programmatic callers and test doubles build their own deps and are not
16
48
  // obliged to carry one, and a courtesy counter must never be the reason a tool call throws.
17
49
  if (opts.reticleRoot === undefined || 0 === opts.reticleRoot.length)
18
- return store;
19
- store ??= new ImpactStore({ ...opts, reticleRoot: opts.reticleRoot });
20
- return store;
50
+ return storeFor(undefined);
51
+ if (defaultRoot === undefined) {
52
+ defaultRoot = opts.reticleRoot;
53
+ const { projectName, now } = opts;
54
+ storeOpts = {
55
+ ...(projectName === undefined ? {} : { projectName }),
56
+ ...(now === undefined ? {} : { now }),
57
+ };
58
+ }
59
+ return storeFor(opts.reticleRoot);
21
60
  }
22
- /** The live store, if one has been initialised. */
23
- export function impactStore() {
24
- return store;
61
+ /** The live store for a root (default when omitted), if one has been initialised. */
62
+ export function impactStore(root) {
63
+ return storeFor(root);
25
64
  }
26
- /** Test seam: drop the singleton so a suite can start from a clean record. */
65
+ /** Test seam: drop the stores so a suite can start from a clean record. */
27
66
  export function resetImpactForTest() {
28
- store = undefined;
67
+ stores.clear();
68
+ defaultRoot = undefined;
69
+ storeOpts = {};
29
70
  }
30
- /** Record one delta, if the recorder is live. Never throws: stats must not break a tool call. */
31
- export function recordImpact(delta, meta = {}) {
71
+ /**
72
+ * Record one delta against a project's ledger. Never throws: stats must not break a tool call.
73
+ *
74
+ * `root` is the `.reticle` directory this call's evidence belongs to. Omitted means "could not tell
75
+ * which project", which falls back to the daemon's own root — the old behaviour, kept for the paths
76
+ * that genuinely have no session to ask.
77
+ */
78
+ export function recordImpact(delta, meta = {}, root) {
32
79
  try {
33
- store?.record(delta, meta);
80
+ storeFor(root)?.record(delta, meta);
34
81
  }
35
82
  catch {
36
83
  // Counting is a courtesy to the user; failing to count is not worth failing their call over.
37
84
  }
38
85
  }
39
- export function impactSnapshot() {
40
- return store?.snapshot();
86
+ export function impactSnapshot(root) {
87
+ return storeFor(root)?.snapshot();
41
88
  }
42
89
  /**
43
90
  * What one tool call did, as counters.
@@ -71,6 +118,42 @@ export function deltaForToolResult(raw, durationMs, refused) {
71
118
  }
72
119
  return delta;
73
120
  }
121
+ /**
122
+ * What one FAILED verdict was, in a line.
123
+ *
124
+ * Only a verified:"no" produces one. That is the same rule the counters follow and the same rule
125
+ * the product states to agents: an "unknown" is not a defect, it is Reticle admitting it could not
126
+ * tell — recording those here would fill the user's short list with the tool's own blind spots.
127
+ *
128
+ * Everything is read from the result the tool already returned, so this costs nothing extra and
129
+ * cannot disagree with the verdict it describes.
130
+ */
131
+ export function defectForToolResult(raw, at) {
132
+ if (!isRecord(raw) || raw['verified'] !== Verified.NO)
133
+ return undefined;
134
+ const effect = isRecord(raw['effect']) ? raw['effect'] : undefined;
135
+ const verdict = isRecord(raw['verdict']) ? raw['verdict'] : undefined;
136
+ const named = text(effect?.['name']) ?? text(effect?.['testid']) ?? text(effect?.['component']);
137
+ const why = text(verdict?.['failureReason']) ?? text(raw['because']);
138
+ const defect = {
139
+ at,
140
+ // The control that was acted on, when the result names one — "Sign In did not sign in" is a
141
+ // sentence somebody can act on; "a verdict failed" is not.
142
+ title: named === undefined ? (why ?? 'a declared consequence did not hold') : named,
143
+ };
144
+ if (named !== undefined && why !== undefined)
145
+ defect.detail = why;
146
+ const source = text(raw['source']);
147
+ if (source !== undefined)
148
+ defect.source = source;
149
+ return defect;
150
+ }
151
+ function text(value) {
152
+ if ('string' !== typeof value)
153
+ return undefined;
154
+ const trimmed = value.trim();
155
+ return 0 === trimmed.length ? undefined : trimmed;
156
+ }
74
157
  function isRecord(value) {
75
158
  return 'object' === typeof value && null !== value;
76
159
  }
@@ -1,8 +1,17 @@
1
- import { type ImpactCounts, type ImpactScope, type ImpactSnapshot } from '@reticlehq/core';
1
+ import { type ImpactCounts, type ImpactDefect, type ImpactScope, type ImpactSnapshot } from '@reticlehq/core';
2
2
  export interface ImpactPaths {
3
3
  project: string;
4
4
  global: string;
5
5
  }
6
+ /**
7
+ * Where this project's dashboard lives, if `reticle link` recorded one.
8
+ *
9
+ * Read from the link file rather than derived from the API url, because the API origin and the
10
+ * console origin are different hosts in every deployment that is not a laptop. Synchronous and
11
+ * best-effort, like every other read in this file: a missing or malformed link file means no link
12
+ * in the HUD, never a failed tool call.
13
+ */
14
+ export declare function readDashboardUrl(reticleRoot: string): string | undefined;
6
15
  /** Read a scope, tolerating absence and corruption: a broken file starts over rather than throwing. */
7
16
  export declare function readScope(path: string, now: number): ImpactScope;
8
17
  /** YYYY-MM-DD in local time - the day boundary a person recognises, not UTC's. */
@@ -17,6 +26,11 @@ export declare function isoDay(now: number): string;
17
26
  */
18
27
  export interface ImpactFoldMeta {
19
28
  runMs?: number;
29
+ /**
30
+ * The defect this call caught, when it caught one. Carried beside the counters rather than in
31
+ * them because it is not a tally: `counts.failed` says how many, this says which.
32
+ */
33
+ defect?: ImpactDefect;
20
34
  }
21
35
  /** Fold one delta into a scope: totals, today's bucket, records and streak. Pure. */
22
36
  export declare function applyDelta(scope: ImpactScope, delta: Partial<ImpactCounts>, now: number, meta?: ImpactFoldMeta): ImpactScope;
@@ -1,7 +1,7 @@
1
1
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
- import { ReticleDir, IMPACT_DAILY_BUCKETS, IMPACT_SCHEMA_VERSION, ImpactScopeSchema, addImpactCounts, emptyImpactCounts, emptyImpactRecords, estimateImpactSavings, } from '@reticlehq/core';
4
+ import { ReticleDir, IMPACT_DAILY_BUCKETS, IMPACT_DEFECT_LIMIT, IMPACT_SCHEMA_VERSION, ImpactScopeSchema, addImpactCounts, emptyImpactCounts, emptyImpactRecords, estimateImpactSavings, } from '@reticlehq/core';
5
5
  /**
6
6
  * Where Reticle keeps the record of what it has done for you.
7
7
  *
@@ -14,6 +14,8 @@ import { ReticleDir, IMPACT_DAILY_BUCKETS, IMPACT_SCHEMA_VERSION, ImpactScopeSch
14
14
  */
15
15
  /** Writes are debounced: a verification loop is 50-200 calls, and each one is a counter bump. */
16
16
  const WRITE_DEBOUNCE_MS = 800;
17
+ /** The project's cloud binding, written by `reticle link`. Absent for an unlinked project. */
18
+ const CLOUD_LINK_FILE = 'cloud.json';
17
19
  /**
18
20
  * `reticleRoot` is the project's own `.reticle` directory (what every other store here is handed);
19
21
  * the global scope always lives beside the daemon's own state in `~/.reticle`.
@@ -24,6 +26,26 @@ function impactPaths(reticleRoot) {
24
26
  global: join(homedir(), ReticleDir.ROOT, ReticleDir.IMPACT_FILE),
25
27
  };
26
28
  }
29
+ /**
30
+ * Where this project's dashboard lives, if `reticle link` recorded one.
31
+ *
32
+ * Read from the link file rather than derived from the API url, because the API origin and the
33
+ * console origin are different hosts in every deployment that is not a laptop. Synchronous and
34
+ * best-effort, like every other read in this file: a missing or malformed link file means no link
35
+ * in the HUD, never a failed tool call.
36
+ */
37
+ export function readDashboardUrl(reticleRoot) {
38
+ try {
39
+ const raw = JSON.parse(readFileSync(join(reticleRoot, CLOUD_LINK_FILE), 'utf8'));
40
+ if ('object' !== typeof raw || null === raw)
41
+ return undefined;
42
+ const value = raw['dashboardUrl'];
43
+ return 'string' === typeof value && value.length > 0 ? value : undefined;
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ }
27
49
  function emptyScope(now) {
28
50
  const counts = emptyImpactCounts();
29
51
  return {
@@ -32,6 +54,7 @@ function emptyScope(now) {
32
54
  records: emptyImpactRecords(),
33
55
  savings: estimateImpactSavings(counts),
34
56
  since: now,
57
+ defects: [],
35
58
  };
36
59
  }
37
60
  /** Read a scope, tolerating absence and corruption: a broken file starts over rather than throwing. */
@@ -105,7 +128,21 @@ export function applyDelta(scope, delta, now, meta = {}) {
105
128
  }
106
129
  records.bestStreakDays = Math.max(records.bestStreakDays, records.streakDays);
107
130
  records.longestRunMs = Math.max(records.longestRunMs, meta.runMs ?? 0);
108
- return { counts, days, records, savings: estimateImpactSavings(counts), since: scope.since };
131
+ /*
132
+ * Newest first, capped. Prepending is what makes the HUD's short list the CURRENT breakage rather
133
+ * than the first ten things that ever broke — which, after a week, is a list about the past.
134
+ */
135
+ const defects = meta.defect === undefined
136
+ ? scope.defects
137
+ : [meta.defect, ...scope.defects].slice(0, IMPACT_DEFECT_LIMIT);
138
+ return {
139
+ counts,
140
+ days,
141
+ records,
142
+ savings: estimateImpactSavings(counts),
143
+ since: scope.since,
144
+ defects,
145
+ };
109
146
  }
110
147
  /**
111
148
  * The live impact record for one project.
@@ -117,6 +154,7 @@ export class ImpactStore {
117
154
  #paths;
118
155
  #now;
119
156
  #projectName;
157
+ #dashboardUrl;
120
158
  #project;
121
159
  #global;
122
160
  #timer;
@@ -125,6 +163,7 @@ export class ImpactStore {
125
163
  this.#paths = impactPaths(opts.reticleRoot);
126
164
  this.#now = opts.now ?? (() => Date.now());
127
165
  this.#projectName = opts.projectName;
166
+ this.#dashboardUrl = readDashboardUrl(opts.reticleRoot);
128
167
  const now = this.#now();
129
168
  this.#project = readScope(this.#paths.project, now);
130
169
  this.#global = readScope(this.#paths.global, now);
@@ -148,6 +187,8 @@ export class ImpactStore {
148
187
  };
149
188
  if (this.#projectName !== undefined)
150
189
  snap.projectName = this.#projectName;
190
+ if (this.#dashboardUrl !== undefined)
191
+ snap.dashboardUrl = this.#dashboardUrl;
151
192
  return snap;
152
193
  }
153
194
  /** Write now (process exit, tests). */
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { join } from 'node:path';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
- import { PROJECT_REGISTRY_FILE, emptyProjectRegistry, parseProjectRegistry } from '@reticlehq/core';
4
+ import { resolveProjectCloud } from './cloud/cloud-config.js';
5
+ import { startSyncDaemon } from './cloud/sync-daemon.js';
6
+ import { PROJECT_REGISTRY_FILE, emptyProjectRegistry, parseProjectRegistry, projectCandidates, } from '@reticlehq/core';
5
7
  import { discoverProjectConfigs } from './cli/config-discovery.js';
6
8
  import { projectCandidatesFrom, resolveArtifactRoot, } from './project/artifact-root.js';
7
9
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -14,6 +16,8 @@ import { openLoopbackAlias } from './daemon/loopback-alias.js';
14
16
  import { reportAppInstrumented } from './telemetry/app-instrumented.js';
15
17
  import { resolveBridgeSecurityWithAutoToken } from './bridge/bridge-security.js';
16
18
  import { Bridge } from './bridge/bridge.js';
19
+ import { sdkFixForDirectory } from './version/sdk-fix.js';
20
+ import { SERVER_VERSION } from './version/server-version.js';
17
21
  import { BaselineStore } from './project/baselines.js';
18
22
  import { RecordingStore } from './flows/recordings.js';
19
23
  import { initImpact } from './impact/impact-recorder.js';
@@ -39,6 +43,7 @@ import { statusPayload } from './status-payload.js';
39
43
  import { CdpRealInputProvider, LaunchedRealInputProvider } from './input/real-input.js';
40
44
  import { cpus } from 'node:os';
41
45
  import { BrowserPool } from './pool/browser-pool.js';
46
+ import { AGENT_ALREADY_DRIVING_ELSEWHERE, shouldGreetWithLeaseNotice, } from './session/lease-visibility.js';
42
47
  import { playwrightLauncher, resolveMaxContexts } from './pool/playwright-launcher.js';
43
48
  import { LeaseReaper } from './pool/lease-reaper.js';
44
49
  import { readJournalEnabled, readProjectId } from './cli/cli-port.js';
@@ -187,7 +192,14 @@ function makeNetworkDetailRouter(bridge, driveUrl) {
187
192
  function attachJournal(bridge, deps) {
188
193
  const journalAttach = makeJournalAttach(deps);
189
194
  const ambientStore = new AmbientStore(deps.fs, deps.reticleRoot);
195
+ // Built once, not per session: the resolver walks config discovery and the user-level registry,
196
+ // and neither changes between two tabs connecting a second apart.
197
+ const resolveArtifactRoot = artifactRootResolver(deps.reticleRoot);
190
198
  bridge.attachSessionCreate((session) => {
199
+ // Stamp the project's own `.reticle` before ANY counter fires for this session. Without it every
200
+ // verdict is recorded against wherever the daemon was started, which is how one app's evidence
201
+ // reached a different account's production dashboard.
202
+ session.artifactRoot = resolveArtifactRoot(session.projectId).root;
191
203
  journalAttach(session);
192
204
  // Seed the learned ambient map so a fresh session starts knowing which regions churn, instead of
193
205
  // re-learning from zero. Best-effort + async: a late seed still helps, a failure is silent.
@@ -256,6 +268,14 @@ async function resolveRealInput(options, onNavigateError, onNetworkDetail) {
256
268
  return {};
257
269
  }
258
270
  /** Start the Reticle bridge (browser WS endpoint) and, by default, the MCP stdio server. */
271
+ /**
272
+ * The SDK-upgrade sentence for THIS project's package.json, evaluated at each HELLO so a
273
+ * just-edited manifest is what we name. Falls back to the framework-neutral sensor when cwd is
274
+ * not an app.
275
+ */
276
+ function sdkFixForCwd() {
277
+ return sdkFixForDirectory(SERVER_VERSION, process.cwd());
278
+ }
259
279
  /**
260
280
  * Resolve a session's artifact root, from everything this machine knows about where projects live.
261
281
  *
@@ -264,6 +284,39 @@ async function resolveRealInput(options, onNavigateError, onNetworkDetail) {
264
284
  * this daemon is up, and a resolution that used a startup snapshot would keep answering with a map
265
285
  * from before the project the agent is now driving existed.
266
286
  */
287
+ /**
288
+ * Every `.reticle` root on this machine that sync should consider, from the same two sources the
289
+ * artifact resolver uses.
290
+ *
291
+ * Read on every call rather than snapshotted, for the same reason: `reticle link` can run in
292
+ * another terminal while this daemon is up, and a startup snapshot would keep that repo silent
293
+ * until somebody restarted a process they have no reason to suspect.
294
+ *
295
+ * Returns directories that MIGHT be linked; the caller resolves each and skips the ones that are
296
+ * not. Deciding that here would mean reading every cloud.json on every tick.
297
+ */
298
+ function knownProjectRoots() {
299
+ const roots = new Set();
300
+ try {
301
+ const path = join(homedir(), ReticleDir.ROOT, PROJECT_REGISTRY_FILE);
302
+ if (existsSync(path)) {
303
+ const registry = parseProjectRegistry(JSON.parse(readFileSync(path, 'utf8')));
304
+ for (const candidate of projectCandidates(registry))
305
+ roots.add(join(candidate.directory, ReticleDir.ROOT));
306
+ }
307
+ }
308
+ catch {
309
+ // A registry that cannot be read is an empty one — never a reason to stop syncing.
310
+ }
311
+ try {
312
+ for (const config of discoverProjectConfigs(process.cwd()).found)
313
+ roots.add(join(config.directory, ReticleDir.ROOT));
314
+ }
315
+ catch {
316
+ // Same: a diagnostic walk that throws must not take the sync loop with it.
317
+ }
318
+ return [...roots];
319
+ }
267
320
  function artifactRootResolver(daemonRoot) {
268
321
  return (projectId) => {
269
322
  let registry = emptyProjectRegistry();
@@ -298,7 +351,7 @@ export async function start(options = {}) {
298
351
  // "nothing recorded yet" over a month of history on disk is the worst version of this feature.
299
352
  initImpact({ reticleRoot: options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT) });
300
353
  const security = await resolveBridgeSecurityWithAutoToken(options);
301
- const bridge = new Bridge({ port, ...security });
354
+ const bridge = new Bridge({ port, sdkFix: sdkFixForCwd, ...security });
302
355
  // Server-authoritative liveness: a Node-side reaper (immune to browser throttling) ends sessions
303
356
  // whose agent has gone idle, so a forgotten/crashed agent never leaves the HUD "running" forever.
304
357
  const reaper = new SessionReaper(bridge.sessions);
@@ -332,6 +385,25 @@ export async function start(options = {}) {
332
385
  pool = createBrowserPool(options.headless ?? true);
333
386
  leaseReaper = new LeaseReaper(pool);
334
387
  leaseReaper.start();
388
+ /*
389
+ * Tell a tab that ARRIVES during a lease why its HUD is silent.
390
+ *
391
+ * The acquire-time notice only reaches tabs already connected, so opening the app — or merely
392
+ * reloading it — while an agent held a lease landed somebody on a dark HUD with nothing
393
+ * explaining it. That is the more common way to hit it, because a person opens the dashboard
394
+ * precisely when they want to watch. Reported as "dashboard is being watched, but the agent chat
395
+ * shows nothing".
396
+ *
397
+ * Registered separately from the journal hook rather than folded into it: this one needs the
398
+ * pool, which does not exist on the no-drive path, and session-create handlers are additive.
399
+ */
400
+ const leasePool = pool;
401
+ bridge.attachSessionCreate((session) => {
402
+ const leasedIds = leasePool.leasedSessionIds();
403
+ const leasedProjects = leasedIds.map((id) => bridge.sessions.get(id)?.projectId);
404
+ if (shouldGreetWithLeaseNotice(session, leasedIds, leasedProjects))
405
+ session.pushNarration(AGENT_ALREADY_DRIVING_ELSEWHERE);
406
+ });
335
407
  const deps = {
336
408
  sessions: bridge.sessions,
337
409
  pool,
@@ -391,14 +463,19 @@ export async function startDaemon(options = {}) {
391
463
  initImpact({ reticleRoot: options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT) });
392
464
  const security = await resolveBridgeSecurityWithAutoToken(options);
393
465
  const shared = createSharedServer(security.token === undefined ? {} : { token: security.token });
394
- const bridge = new Bridge({ port, server: shared.httpServer, ...security });
466
+ const bridge = new Bridge({ port, server: shared.httpServer, sdkFix: sdkFixForCwd, ...security });
395
467
  // The daemon owns listen (below), so the real bind error is reported there; absorb bridge.ready's
396
468
  // mirror rejection so a port collision can't surface as an unhandled promise rejection.
397
469
  void bridge.ready.catch(() => undefined);
470
+ // Declared before attachStatus so the closure below can report it; assigned further down, before
471
+ // listen — the first status request cannot arrive until after the bind.
472
+ let verifyHttp;
398
473
  // `reticle status` GETs this for a live, at-a-glance view of connected tabs + their health.
399
474
  // The same diagnosis agents get on an empty `reticle_sessions`, so `reticle status` — the
400
475
  // most-run command in the field — stops answering "sessionCount: 0" and nothing else.
401
- shared.attachStatus(() => statusPayload(bridge.sessions.count(), bridge.sessions.list(), bridge.sessions.noSessionHint()));
476
+ // `verifyPort` rides along so a later `serve --http` can tell whether this daemon already honours
477
+ // the requested `--http-port` instead of silently ignoring the flag (#687).
478
+ shared.attachStatus(() => statusPayload(bridge.sessions.count(), bridge.sessions.list(), bridge.sessions.noSessionHint(), verifyHttp?.port));
402
479
  // Agent-independent presence: the daemon outlives any single agent, so when the LAST agent's MCP
403
480
  // connection drops (it stopped, or is waiting on the human), end every session and push a clear
404
481
  // "go to your terminal" notice to the panel — the human is on the browser and must not lose a typed
@@ -426,6 +503,23 @@ export async function startDaemon(options = {}) {
426
503
  const pool = createBrowserPool(options.headless ?? true);
427
504
  const leaseReaper = new LeaseReaper(pool);
428
505
  leaseReaper.start();
506
+ /*
507
+ * Automatic cloud sync, for the session nobody thinks about.
508
+ *
509
+ * The daemon outlives every individual tool call and is already running for the whole session, so
510
+ * it is the only place "keep the dashboard current while work is happening" costs nobody an extra
511
+ * process to remember. Safe for an UNLINKED project: it resolves the link on every tick and does
512
+ * nothing until one exists, so `reticle link` takes effect without a restart.
513
+ */
514
+ const cloudSync = startSyncDaemon({
515
+ reticleRoot,
516
+ cloud: () => resolveProjectCloud(fs, reticleRoot, homedir(), process.env),
517
+ // Every OTHER linked repo on this machine, not just the directory the daemon was started in.
518
+ // One daemon serves many projects, and pushing only its own root left the rest silently
519
+ // reporting nothing — indistinguishable, on the dashboard, from nobody having verified anything.
520
+ otherRoots: () => Promise.resolve(knownProjectRoots()),
521
+ cloudFor: (root) => resolveProjectCloud(fs, root, homedir(), process.env),
522
+ });
429
523
  // Scope auto-selection to the active project (from .reticle.json) so a stray tab from another app is
430
524
  // never picked when the agent omits a sessionId. Explicit per-call scope/sessionId still overrides.
431
525
  // Scope + the no-session diagnosis: "no browser session connected" is the error that ends most
@@ -433,7 +527,7 @@ export async function startDaemon(options = {}) {
433
527
  // Ordered AFTER the pool: the watch may open a browser itself the moment it finds a listening dev
434
528
  // server for a wired project (see no-session-watch.ts), and it opens it through the pool — the same
435
529
  // path reticle_lease takes, in this process, binding nothing.
436
- wireSessionScope(bridge.sessions, readProjectId(process.cwd()), port, () => pool.reapedLeaseCount(), (url) => pool.acquire(url));
530
+ wireSessionScope(bridge.sessions, readProjectId(process.cwd()), port, (sessionId) => pool.wasReapedLease(sessionId), (url) => pool.acquire(url));
437
531
  const deps = {
438
532
  sessions: bridge.sessions,
439
533
  pool,
@@ -444,9 +538,18 @@ export async function startDaemon(options = {}) {
444
538
  project,
445
539
  fs,
446
540
  reticleRoot,
541
+ // The long-lived daemon needs this MORE than the standalone MCP process does, not less: it is
542
+ // the one that outlives a single project and serves every app on the machine. Omitting it here
543
+ // silently disabled per-session artifact resolution for every agent that attaches to a running
544
+ // daemon — the common case — so flows were listed, loaded and healed against wherever the
545
+ // daemon happened to be launched. Wired in only one of the two places, a resolver is a resolver
546
+ // that does not run.
547
+ artifactRootFor: artifactRootResolver(reticleRoot),
447
548
  now,
448
549
  bridgePort: port,
449
550
  browserProbe: probeChromium,
551
+ // A finished verification should not sit behind a one-minute timer — see ToolDeps.onRunPersisted.
552
+ onRunPersisted: () => cloudSync.nudge(),
450
553
  };
451
554
  const profile = resolveToolSurface(options.toolProfile);
452
555
  const effectiveDeps = realInput !== undefined ? { ...deps, realInput } : deps;
@@ -461,9 +564,11 @@ export async function startDaemon(options = {}) {
461
564
  // Optional OEM/CI verify endpoint: a host platform POSTs to /verify and gets an ReticleVerificationRun,
462
565
  // driving the same flow-replay machinery the agent uses — no MCP stdio, no human. Each verdict is
463
566
  // persisted via RunStore. Localhost-bound + token-guarded. Off unless `reticle serve --http`.
464
- let verifyHttp;
567
+ // (`verifyHttp` itself is declared above attachStatus, which reports its port.)
465
568
  if (true === options.httpVerify) {
466
- const runStore = new RunStore(fs, reticleRoot);
569
+ // Wakes cloud sync when the HTTP verify server persists a run — that path does not push
570
+ // inline the way the MCP one does, so without this its runs waited for the timer.
571
+ const runStore = new RunStore(fs, reticleRoot, { onWrote: () => cloudSync.nudge() });
467
572
  const runner = new ReticleRunner(createRunnerPort(effectiveDeps));
468
573
  const token = options.httpVerifyToken ?? process.env[ReticleEnv.VERIFY_TOKEN] ?? '';
469
574
  verifyHttp = await startVerifyServer({ runner, token, persist: (run) => runStore.write(run) }, options.httpVerifyPort ?? RETICLE_VERIFY_DEFAULT_PORT);
@@ -552,6 +657,7 @@ export async function startDaemon(options = {}) {
552
657
  if (vh !== undefined)
553
658
  await new Promise((resolve) => vh.server.close(() => resolve()));
554
659
  leaseReaper.stop();
660
+ cloudSync.stop();
555
661
  await loopbackAlias.close?.();
556
662
  await pool.shutdown();
557
663
  await owned?.dispose();
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `reticle init` writes a behavioral rule into the coding agent's instruction file so the agent
3
3
  * VERIFIES a feature with Reticle after building it — not only when the user remembers to say "test
4
- * it with reticle". Registering the MCP server (mcp.ts / cursor.ts) only makes the tools AVAILABLE;
4
+ * it with reticle". Registering the MCP server (mcp.ts / mcp-clients.ts) only makes the tools AVAILABLE;
5
5
  * this is the piece that makes the agent reach for them, every session, unprompted.
6
6
  *
7
7
  * It carries a SECOND standing instruction, and that one is not about this project at all: send
@@ -15,7 +15,7 @@
15
15
  * written into whichever instruction file the detected agent reads: `CLAUDE.md` (Claude Code),
16
16
  * `.cursor/rules/reticle.mdc` (Cursor, `alwaysApply` so it stays in context), or `AGENTS.md` as the
17
17
  * cross-agent fallback. Idempotent via a marker block — safe to append to an existing file and a
18
- * re-run is a no-op. Mirrors the apply/already result shape used across init (see cursor.ts).
18
+ * re-run is a no-op. Mirrors the apply/already result shape used across init (see mcp-clients.ts).
19
19
  */
20
20
  /** Project-root instruction files the agent re-reads every session. */
21
21
  export declare const CLAUDE_MD_PATH = "CLAUDE.md";
@@ -69,13 +69,20 @@ export declare const CLI: string;
69
69
  * inside a host that asks the human for permission — so a dev server it starts is visible in the
70
70
  * transcript, attributable, and stoppable.
71
71
  *
72
- * The guards are the whole reason this is safe, so no renderer may drop them: adopt what is already
73
- * listening, read the command rather than invent one, never kill, background it and SAY so, and leave
74
- * the permission prompt to the host.
72
+ * The guards are the whole reason this is safe, so no renderer may drop them: one server at a time,
73
+ * read the command rather than invent one, never kill anything you did not start, and leave the
74
+ * permission prompt to the host. "Background it and say so" is no longer among them because the
75
+ * branch above now states it at the point of action, and this text is read every turn — a rule
76
+ * stated twice costs every agent tokens forever and is the first thing to drift.
77
+ *
78
+ * The RESTART branch outranks the rest and is stated first. It used to be an aside, while "if a dev
79
+ * server is already listening, use it" was a numbered guard — so an agent resolving the two
80
+ * reasonably reused a server started before `init` edited the build config, served a bundle with no
81
+ * SDK in it, and landed on an empty session list with every other step done correctly.
75
82
  *
76
83
  * Exported because slash-command.ts renders the same rule; there must not be two versions of it.
77
84
  */
78
- export declare const DEV_SERVER_POLICY = "**If no dev server is listening, start one yourself.** Read the project's own dev script out of `package.json` (`dev`, `start`, whatever this project calls it), run it in the BACKGROUND, tell the user in one line that it is running and how to stop it, then carry on. Stopping to ask is how a verification turn ends with nothing verified.\n\nFive guards, none optional:\n\n1. **Never start a second one.** If something is already listening on the app's port, use it.\n2. **Never guess the command.** It comes from `package.json` scripts. If there is no recognisable dev script, say so and stop rather than inventing one.\n3. **Never kill anything.** Not a dev server, not a daemon, not a port holder \u2014 including one you started.\n4. **Background it, and say so.** The user must know a server is running and how to stop it. A dev server the human does not know about is the same failure one step later.\n5. **The permission prompt belongs to your host.** Never try to bypass, suppress or auto-approve it, and take a refusal as the answer.";
85
+ export declare const DEV_SERVER_POLICY = "**A dev server already running when `reticle init` ran does not have Reticle in its bundle.** It read the build config at boot; `init` edited it afterwards. It serves the old bundle and no session appears. In order:\n\n1. **A dev server was already running?** Restart it, then hard-reload the tab. \"Something is listening\" does not mean the right bundle is served.\n2. **Nothing was running?** Start it in the BACKGROUND and say so in one line. `reticle_sessions` gives you this project's own dev command in `next_action`; use that, never compose one. Started after `init`, it needs no restart.\n\nStopping to ask is how a verification turn ends with nothing verified.\n\nFour guards, none optional:\n\n1. **Never run two at once.** One dev server on the app's port. Restarting a stale one means stopping it first, not starting a second alongside it.\n2. **Never guess the command.** It comes from `package.json` scripts. No recognisable dev script means say so and stop, not invent one.\n3. **Never kill anything you did not start**, and never a daemon or a port holder. The one exception is the restart above, and say in one line that you did it.\n4. **The permission prompt belongs to your host.** Never bypass, suppress or auto-approve it, and take a refusal as the answer.";
79
86
  /**
80
87
  * The operative rules, carried by every file that loads on every turn.
81
88
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `reticle init` writes a behavioral rule into the coding agent's instruction file so the agent
3
3
  * VERIFIES a feature with Reticle after building it — not only when the user remembers to say "test
4
- * it with reticle". Registering the MCP server (mcp.ts / cursor.ts) only makes the tools AVAILABLE;
4
+ * it with reticle". Registering the MCP server (mcp.ts / mcp-clients.ts) only makes the tools AVAILABLE;
5
5
  * this is the piece that makes the agent reach for them, every session, unprompted.
6
6
  *
7
7
  * It carries a SECOND standing instruction, and that one is not about this project at all: send
@@ -15,7 +15,7 @@
15
15
  * written into whichever instruction file the detected agent reads: `CLAUDE.md` (Claude Code),
16
16
  * `.cursor/rules/reticle.mdc` (Cursor, `alwaysApply` so it stays in context), or `AGENTS.md` as the
17
17
  * cross-agent fallback. Idempotent via a marker block — safe to append to an existing file and a
18
- * re-run is a no-op. Mirrors the apply/already result shape used across init (see cursor.ts).
18
+ * re-run is a no-op. Mirrors the apply/already result shape used across init (see mcp-clients.ts).
19
19
  */
20
20
  import { NPX } from './mcp.js';
21
21
  import { RETICLE_NPM_PACKAGE } from '../version/server-version.js';
@@ -76,21 +76,32 @@ export const CLI = `${NPX} ${RETICLE_NPM_PACKAGE}`;
76
76
  * inside a host that asks the human for permission — so a dev server it starts is visible in the
77
77
  * transcript, attributable, and stoppable.
78
78
  *
79
- * The guards are the whole reason this is safe, so no renderer may drop them: adopt what is already
80
- * listening, read the command rather than invent one, never kill, background it and SAY so, and leave
81
- * the permission prompt to the host.
79
+ * The guards are the whole reason this is safe, so no renderer may drop them: one server at a time,
80
+ * read the command rather than invent one, never kill anything you did not start, and leave the
81
+ * permission prompt to the host. "Background it and say so" is no longer among them because the
82
+ * branch above now states it at the point of action, and this text is read every turn — a rule
83
+ * stated twice costs every agent tokens forever and is the first thing to drift.
84
+ *
85
+ * The RESTART branch outranks the rest and is stated first. It used to be an aside, while "if a dev
86
+ * server is already listening, use it" was a numbered guard — so an agent resolving the two
87
+ * reasonably reused a server started before `init` edited the build config, served a bundle with no
88
+ * SDK in it, and landed on an empty session list with every other step done correctly.
82
89
  *
83
90
  * Exported because slash-command.ts renders the same rule; there must not be two versions of it.
84
91
  */
85
- export const DEV_SERVER_POLICY = `**If no dev server is listening, start one yourself.** Read the project's own dev script out of \`package.json\` (\`dev\`, \`start\`, whatever this project calls it), run it in the BACKGROUND, tell the user in one line that it is running and how to stop it, then carry on. Stopping to ask is how a verification turn ends with nothing verified.
92
+ export const DEV_SERVER_POLICY = `**A dev server already running when \`reticle init\` ran does not have Reticle in its bundle.** It read the build config at boot; \`init\` edited it afterwards. It serves the old bundle and no session appears. In order:
93
+
94
+ 1. **A dev server was already running?** Restart it, then hard-reload the tab. "Something is listening" does not mean the right bundle is served.
95
+ 2. **Nothing was running?** Start it in the BACKGROUND and say so in one line. \`reticle_sessions\` gives you this project's own dev command in \`next_action\`; use that, never compose one. Started after \`init\`, it needs no restart.
96
+
97
+ Stopping to ask is how a verification turn ends with nothing verified.
86
98
 
87
- Five guards, none optional:
99
+ Four guards, none optional:
88
100
 
89
- 1. **Never start a second one.** If something is already listening on the app's port, use it.
90
- 2. **Never guess the command.** It comes from \`package.json\` scripts. If there is no recognisable dev script, say so and stop rather than inventing one.
91
- 3. **Never kill anything.** Not a dev server, not a daemon, not a port holder — including one you started.
92
- 4. **Background it, and say so.** The user must know a server is running and how to stop it. A dev server the human does not know about is the same failure one step later.
93
- 5. **The permission prompt belongs to your host.** Never try to bypass, suppress or auto-approve it, and take a refusal as the answer.`;
101
+ 1. **Never run two at once.** One dev server on the app's port. Restarting a stale one means stopping it first, not starting a second alongside it.
102
+ 2. **Never guess the command.** It comes from \`package.json\` scripts. No recognisable dev script means say so and stop, not invent one.
103
+ 3. **Never kill anything you did not start**, and never a daemon or a port holder. The one exception is the restart above, and say in one line that you did it.
104
+ 4. **The permission prompt belongs to your host.** Never bypass, suppress or auto-approve it, and take a refusal as the answer.`;
94
105
  /**
95
106
  * The operative rules, carried by every file that loads on every turn.
96
107
  *
@@ -15,3 +15,19 @@ import { type SourcePatch } from './patch-kind.js';
15
15
  import { UiLibrary } from './detect.js';
16
16
  export declare function patchAstroConfig(source: string): SourcePatch;
17
17
  export declare function patchAstroLayout(source: string, port: number | undefined, projectId: string | undefined, uiLibrary?: UiLibrary): SourcePatch;
18
+ /** Where Astro keeps ambient types — `create-astro` ships this file. */
19
+ export declare const ASTRO_ENV_DTS_PATH = "src/env.d.ts";
20
+ /**
21
+ * The declarations `astro check` needs for the Vite `define` names (#677).
22
+ *
23
+ * Matching SvelteKit's `hooks.client.ts` typing: `string | undefined`, because the define can be
24
+ * absent when the pairing token file is missing.
25
+ */
26
+ export declare const ASTRO_ENV_DTS_DECLARES: string;
27
+ /**
28
+ * Append the Vite-define ambient declarations to `src/env.d.ts`, or create the file.
29
+ *
30
+ * Without this, `create-astro`'s default `"astro check && astro build"` fails with four
31
+ * `ts(2304) Cannot find name '__RETICLE_TOKEN__'` errors after a clean init (#677).
32
+ */
33
+ export declare function patchAstroEnvDts(existing: string | null): SourcePatch;