@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
package/dist/cli.js CHANGED
@@ -5,19 +5,24 @@ import { realpathSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
  import { stateDirProblem } from './daemon/state-dir.js';
7
7
  import { statusNextAction } from './cli/status-next-action.js';
8
- import { hasConnectedBefore } from './session/connection-memory.js';
8
+ import { readDevServers } from './daemon/dev-servers.js';
9
+ import { hasConnectedBefore, hasProjectConnectedBefore } from './session/connection-memory.js';
9
10
  import { attachStatusFields } from './mcp/attach-memory.js';
10
11
  import { reticleStateHome } from './daemon/daemon.js';
11
12
  import { handleMcp } from './cli/mcp-command.js';
12
- import { resolveDaemonForProject } from './daemon/daemon-resolve.js';
13
+ import { daemonSpawnArgs, daemonStartOptions } from './cli/daemon-start-options.js';
14
+ import { daemonsServingProjectElsewhere, resolveDaemonForProject, splitBrainNote, wrongDaemonNote, } from './daemon/daemon-resolve.js';
13
15
  import { handleWatch, handleCapsules, handleGate, loadNamedFlows, resolveChangedFiles, } from './cli/cli-flow-commands.js';
14
- import { RETICLE_DEFAULT_PORT, ReticleDir, ReticleEnv } from '@reticlehq/core';
16
+ import { RETICLE_DEFAULT_PORT, ReticleDir, ReticleEnv, devServersForProject, } from '@reticlehq/core';
15
17
  import { loadDotEnv } from './telemetry/dev-repo.js';
18
+ import { licenseKeyFromEnvFiles } from './license/license-env.js';
19
+ import { LICENSE_KEY_ENV } from './license/license.js';
16
20
  import { createNodeFileSystem } from './project/fs-port.js';
17
21
  import { affectedSavedFlows } from './flows/flow-sources.js';
18
22
  import { availableUpdate } from './update/update-nudge.js';
19
23
  import { handleUpdate, handleRollback } from './cli/cli-update-commands.js';
20
- import { startDaemon } from './index.js';
24
+ import { startDaemon, RETICLE_VERIFY_DEFAULT_PORT } from './index.js';
25
+ import { verifyEndpointMismatch } from './status-payload.js';
21
26
  import { isCloudCommand, runCloudCommand } from './cli/cloud-cli.js';
22
27
  import { SERVER_VERSION } from './version/server-version.js';
23
28
  import { log } from './log.js';
@@ -34,12 +39,12 @@ import { handleVerify } from './cli/cli-verify.js';
34
39
  import { runKill } from './cli/cli-kill.js';
35
40
  import { summarizeHunt } from './hunt/hunt-report.js';
36
41
  import { runInit } from './init/run.js';
37
- import { confirmInstall, nodeConfirmDeps } from './init/confirm.js';
42
+ import { continueAfterInit } from './setup/init-runtime.js';
38
43
  import { handleDoctor } from './cli/cli-doctor.js';
39
44
  import { buildNodeIo } from './init/node-io.js';
40
45
  import { describeLicense } from './license/license.js';
41
46
  import { isLikelyDevServerPort, devServerPortWarning, readProjectPort, readProjectId, } from './cli/cli-port.js';
42
- import { DAEMON_INNER_COMMAND, PORT_FLAG, DRIVE_FLAG, HEADED_FLAG, HTTP_FLAG, HTTP_PORT_FLAG, HTTP_TOKEN_FLAG, parseCliArgs, CLI_USAGE, } from './cli/cli-parse.js';
47
+ import { DAEMON_INNER_COMMAND, PORT_FLAG, parseCliArgs, CLI_USAGE } from './cli/cli-parse.js';
43
48
  import { handleFeedback, handleIdentify, handleTelemetry } from './telemetry/feedback-cli.js';
44
49
  import { installDaemonTelemetry } from './telemetry/daemon-telemetry.js';
45
50
  import { reportCliRun } from './telemetry/cli-telemetry.js';
@@ -55,14 +60,13 @@ function handleInit(parsed) {
55
60
  dryRun: parsed.dryRun,
56
61
  install: parsed.install,
57
62
  ...(parsed.app === undefined ? {} : { app: parsed.app }),
63
+ captureBodies: true === parsed.captureBodies,
58
64
  // The outcome is reported by confirmInstall instead, once it knows whether an app connected —
59
65
  // `init` writing files was never the same thing as `init` working (#269).
60
66
  deferOutcome: true,
67
+ continuesToRuntime: true !== parsed.filesOnly && true !== parsed.dryRun,
61
68
  }, io);
62
- void confirmInstall(result, io, nodeConfirmDeps(parsed.port ?? RETICLE_DEFAULT_PORT)).then(() => {
63
- if (!result.ok)
64
- process.exit(1);
65
- });
69
+ void continueAfterInit(parsed, result, io, cwd);
66
70
  }
67
71
  function handleServe(parsed) {
68
72
  void serveWithHonestExit(parsed);
@@ -84,6 +88,20 @@ async function serveWithHonestExit(parsed) {
84
88
  status: fetchStatus,
85
89
  });
86
90
  if (presence === PortPresence.DAEMON) {
91
+ // A daemon that is already up was started with ITS flags, not these — `serve` only attaches.
92
+ // Exiting 0 here regardless is how `--http-port` came to be accepted and ignored (#687): the
93
+ // running daemon kept serving whatever it was started with, and the flag vanished without a
94
+ // word. Ask the daemon which verify port it actually serves and refuse when it is not the one
95
+ // requested — a flag that cannot be honoured must say so, not report success.
96
+ if (parsed.http) {
97
+ const mismatch = verifyEndpointMismatch(await fetchStatus(parsed.port), parsed.httpPort ?? RETICLE_VERIFY_DEFAULT_PORT);
98
+ if (mismatch !== undefined) {
99
+ log('reticle_daemon_start_refused', { port: parsed.port, reason: mismatch });
100
+ process.stderr.write(`${mismatch}\n`);
101
+ process.exit(1);
102
+ return;
103
+ }
104
+ }
87
105
  log('reticle_daemon_already_running', { port: parsed.port });
88
106
  return;
89
107
  }
@@ -103,19 +121,7 @@ async function serveWithHonestExit(parsed) {
103
121
  process.exit(1);
104
122
  return;
105
123
  }
106
- const daemonArgs = [DAEMON_INNER_COMMAND, PORT_FLAG, String(parsed.port)];
107
- if (parsed.driveUrl !== undefined) {
108
- daemonArgs.push(DRIVE_FLAG, parsed.driveUrl);
109
- if (!parsed.headless)
110
- daemonArgs.push(HEADED_FLAG);
111
- }
112
- if (parsed.http) {
113
- daemonArgs.push(HTTP_FLAG);
114
- if (parsed.httpPort !== undefined)
115
- daemonArgs.push(HTTP_PORT_FLAG, String(parsed.httpPort));
116
- if (parsed.httpToken !== undefined)
117
- daemonArgs.push(HTTP_TOKEN_FLAG, parsed.httpToken);
118
- }
124
+ const daemonArgs = daemonSpawnArgs(parsed);
119
125
  spawnDaemon(process.execPath, scriptPath, daemonArgs, parsed.port);
120
126
  // The child binds asynchronously and, when it cannot, exits 1 long after this process would have
121
127
  // reported success. Wait for it to ANSWER — `/status` responding is the only evidence a daemon
@@ -232,7 +238,20 @@ function withNextAction(facts) {
232
238
  const next = statusNextAction(facts);
233
239
  return next === undefined ? {} : { nextAction: next };
234
240
  }
235
- function handleStatus(port) {
241
+ /**
242
+ * What `status` says about a project whose daemons have split in two.
243
+ *
244
+ * Both halves in one place because they are one condition asked from two positions, and a command
245
+ * can be standing on either. Silent — no key at all — when there is nothing to report, so a healthy
246
+ * run reads exactly as it did.
247
+ */
248
+ function splitBrainFields(port, projectId) {
249
+ const home = reticleStateHome();
250
+ const elsewhere = splitBrainNote(port, daemonsServingProjectElsewhere(projectId, port, home, isAlive, (other) => hasProjectConnectedBefore(home, other, projectId)));
251
+ const note = elsewhere ?? wrongDaemonNote(port, resolveDaemonForProject(projectId, home, isAlive));
252
+ return note === undefined ? {} : { splitBrain: note };
253
+ }
254
+ export async function handleStatus(port) {
236
255
  const pid = readPid(port);
237
256
  // Durable, so it survives the daemon idling out — which is the state `status` is most often run in.
238
257
  const projectId = readProjectId(process.cwd());
@@ -245,51 +264,90 @@ function handleStatus(port) {
245
264
  // Reticle and never lists its tools looks exactly like an abandoned install from here — same idle
246
265
  // daemon, same zero sessions — and the user sees a tool that does nothing. See attach-memory.ts.
247
266
  const client = attachStatusFields(reticleStateHome(), port);
248
- if (null === pid || !isAlive(pid)) {
249
- // `running: false` on its own has been reported about a port that was demonstrably occupied,
250
- // because the pid file is not the port. Ask the port before answering.
251
- void probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus }).then((presence) => {
252
- const running = presenceIsUsable(presence);
253
- log('reticle_status', {
254
- port,
255
- running,
256
- presence,
257
- ...(presence === PortPresence.FOREIGN ? { reason: describePresence(presence, port) } : {}),
258
- // `init` promises this command says why the app has not connected. Without it the answer was
259
- // `running: false` and nothing else, which reads as "Reticle is broken" for what is usually
260
- // just a daemon that has not been asked to do anything yet.
261
- ...withNextAction({ running, sessionCount: 0, previouslyConnected, initialized }),
262
- ...client,
263
- });
267
+ // What the dev servers themselves said. The reason this command could previously only guess at
268
+ // whether the app was running is that it had no way to see one; now the ones carrying Reticle
269
+ // announce, and the advice below stops contradicting the terminal the reader is looking at.
270
+ // Scoped to THIS project: the registry is machine-wide, and an unrelated app's dev server must
271
+ // not make this project look like it is running.
272
+ const devServerPorts = devServersForProject(readDevServers(reticleStateHome()), {
273
+ projectId,
274
+ root: process.cwd(),
275
+ }).map((d) => d.port);
276
+ // The failure where every individual check is green and the chain is broken: the agent's proxy and
277
+ // the app resolved their ports independently and landed on two different daemons. Reported from
278
+ // BOTH stances, because neither one can see it alone.
279
+ const split = splitBrainFields(port, projectId);
280
+ // Doctor and status must answer the same liveness question. A live pid only says that some
281
+ // process exists; a Reticle daemon is running only when the shared port probe reaches /status.
282
+ const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
283
+ if (!presenceIsUsable(presence)) {
284
+ log('reticle_status', {
285
+ port,
286
+ running: false,
287
+ presence,
288
+ ...(presence === PortPresence.FOREIGN ? { reason: describePresence(presence, port) } : {}),
289
+ // `init` promises this command says why the app has not connected. Without it the answer was
290
+ // `running: false` and nothing else, which reads as "Reticle is broken" for what is usually
291
+ // just a daemon that has not been asked to do anything yet.
292
+ ...withNextAction({
293
+ running: false,
294
+ sessionCount: 0,
295
+ previouslyConnected,
296
+ initialized,
297
+ devServerPorts,
298
+ }),
299
+ ...client,
300
+ ...split,
264
301
  });
265
302
  return;
266
303
  }
267
- // The daemon is up — ask it for live sessions + health so status is at-a-glance, not just a pid.
268
- void fetchStatus(port).then((payload) => {
269
- // `status` is the second-most-run command and the one a HUMAN types. The update nudge otherwise
270
- // only rides a tool result, which the majority of daemons never produce — so the people most in
271
- // need of an upgrade were the ones with no path to hearing about it.
272
- const update = availableUpdate();
273
- const nudge = update === undefined ? {} : { updateAvailable: update };
274
- if (payload === undefined) {
275
- log('reticle_status', {
276
- port,
304
+ // The daemon answered the same probe doctor trusts — ask it for live sessions + health.
305
+ const payload = await fetchStatus(port);
306
+ // `status` is the second-most-run command and the one a HUMAN types. The update nudge otherwise
307
+ // only rides a tool result, which the majority of daemons never produce — so the people most in
308
+ // need of an upgrade were the ones with no path to hearing about it.
309
+ const update = availableUpdate();
310
+ const nudge = update === undefined ? {} : { updateAvailable: update };
311
+ if (payload === undefined) {
312
+ log('reticle_status', {
313
+ port,
314
+ running: true,
315
+ pid,
316
+ ...nudge,
317
+ ...withNextAction({
277
318
  running: true,
278
- pid,
279
- ...nudge,
280
- ...withNextAction({ running: true, sessionCount: 0, previouslyConnected, initialized }),
281
- ...client,
282
- });
283
- return;
284
- }
285
- const summary = summarizeStatus(payload);
286
- // Only when the daemon did NOT already explain itself. It has the whole diagnosis in-process and
287
- // puts it on the wire as `why`; printing a second, thinner opinion beside it risks two confident
288
- // answers pointing different ways, which is worse than one.
289
- const next = summary.why === undefined
290
- ? withNextAction({ running: true, ...summary, previouslyConnected, initialized })
291
- : {};
292
- log('reticle_status', { port, running: true, pid, ...summary, ...next, ...client, ...nudge });
319
+ sessionCount: 0,
320
+ previouslyConnected,
321
+ initialized,
322
+ devServerPorts,
323
+ }),
324
+ ...client,
325
+ ...split,
326
+ });
327
+ return;
328
+ }
329
+ const summary = summarizeStatus(payload);
330
+ // Only when the daemon did NOT already explain itself. It has the whole diagnosis in-process and
331
+ // puts it on the wire as `why`; printing a second, thinner opinion beside it risks two confident
332
+ // answers pointing different ways, which is worse than one.
333
+ const next = summary.why === undefined
334
+ ? withNextAction({
335
+ running: true,
336
+ ...summary,
337
+ previouslyConnected,
338
+ initialized,
339
+ devServerPorts,
340
+ })
341
+ : {};
342
+ log('reticle_status', {
343
+ port,
344
+ running: true,
345
+ pid,
346
+ ...summary,
347
+ ...next,
348
+ ...client,
349
+ ...split,
350
+ ...nudge,
293
351
  });
294
352
  }
295
353
  /** Print the running package version (resolved once in server-version.ts). */
@@ -492,25 +550,13 @@ function handleOpen(requestedPort, url) {
492
550
  });
493
551
  }
494
552
  function handleDaemonInner(parsed) {
495
- const options = {
496
- port: parsed.port,
497
- ...(parsed.driveUrl !== undefined
498
- ? { driveUrl: parsed.driveUrl, headless: parsed.headless }
499
- : {}),
500
- ...(parsed.http
501
- ? {
502
- httpVerify: true,
503
- ...(parsed.httpPort !== undefined ? { httpVerifyPort: parsed.httpPort } : {}),
504
- ...(parsed.httpToken !== undefined ? { httpVerifyToken: parsed.httpToken } : {}),
505
- }
506
- : {}),
507
- };
553
+ const options = daemonStartOptions(parsed);
508
554
  startDaemon(options)
509
555
  .then((server) => {
510
556
  log('reticle_daemon_ready', { port: parsed.port, pid: process.pid });
511
557
  // Daemon lifecycle telemetry: started, a one-shot project profile, the periodic counter flush,
512
558
  // and the rich summary on shutdown. All of it lives in one module — see daemon-telemetry.ts.
513
- const daemonTelemetry = installDaemonTelemetry(process.cwd());
559
+ const daemonTelemetry = installDaemonTelemetry(process.cwd(), undefined, parsed.port);
514
560
  // Publish to the discovery registry so a build plugin can find this daemon by projectId — no
515
561
  // hand-reconciled port. Written from the child (only it knows its cwd); removePid drops it.
516
562
  const registryProjectId = readProjectId(process.cwd());
@@ -607,6 +653,14 @@ function main() {
607
653
  // Before anything reads process.env — notably the telemetry gate and the bridge's security
608
654
  // options — fold in a project-local `.env`. Values already in the environment always win.
609
655
  loadDotEnv(process.cwd());
656
+ // The licence key specifically is searched for HARDER than the rest of the environment, because
657
+ // the daemon is spawned without an explicit cwd and inherits the editor's. A key in the app's own
658
+ // `.env` — or in the repo root when the editor started inside the app — was never read, and the
659
+ // customer then produced events that said `missing`, which is indistinguishable from having no
660
+ // licence at all. Only the key is taken; see license-env.ts for why nothing else is.
661
+ const licenseKey = licenseKeyFromEnvFiles(process.cwd());
662
+ if (licenseKey !== undefined)
663
+ process.env[LICENSE_KEY_ENV] = licenseKey;
610
664
  const argv = process.argv.slice(2);
611
665
  // Every invocation passes through here — the single chokepoint for the "how often is it used / how
612
666
  // many distinct machines + projects" metrics. Fire-and-forget: a metric must never delay or fail a run.
@@ -677,7 +731,7 @@ function main() {
677
731
  void handleRestart(parsed.port, parsed.force);
678
732
  break;
679
733
  case 'status':
680
- handleStatus(parsed.port);
734
+ void handleStatus(parsed.port);
681
735
  break;
682
736
  case 'license':
683
737
  handleLicense();
@@ -1,7 +1,8 @@
1
1
  import type { FileSystemPort } from '../project/fs-port.js';
2
2
  import { type CloudConfig } from './cloud-sync.js';
3
3
  /** `<reticleRoot>/cloud.json` — the project's cloud binding + sync policy (non-secret). */
4
- export declare const CLOUD_LINK_FILE = "cloud.json";
4
+ /** Re-exported so the existing importers keep working; the string itself lives in core. */
5
+ export declare const CLOUD_LINK_FILE: "cloud.json";
5
6
  /** `~/.reticle/credentials.json` — the user's per-cloud-project API keys (the only secret). */
6
7
  export declare const CREDENTIALS_FILE = "credentials.json";
7
8
  /** What the daemon mirrors to the cloud for a project. Each surface is independently toggleable. */
@@ -27,6 +28,26 @@ export interface ProjectCloud {
27
28
  /** The cloud project id this project is linked to (for display/logging); null when env-fallback. */
28
29
  projectId: string | null;
29
30
  }
31
+ /** Look up the API key for a cloud project id in the user credentials map. */
32
+ /**
33
+ * The API key this machine holds for one project ON ONE CLOUD.
34
+ *
35
+ * The store used to be keyed by cloud project id ALONE, which collides in the case that is not rare
36
+ * at all — it is the default. `reticle link` names every project "default", so a repo linked to a
37
+ * self-hosted install and a repo linked to the hosted service both claim the slot `default`, and
38
+ * whichever was linked last wins. Measured: two repos on one machine, and the production key was
39
+ * being sent to a localhost server, which answered 401.
40
+ *
41
+ * That is the same class of defect as sending a session token to a host that did not issue it, and
42
+ * it deserves the same answer: a credential is scoped to the cloud that minted it, and no credential
43
+ * at all is better than one belonging to somewhere else.
44
+ *
45
+ * Two shapes are accepted. `{ key, url }` is what is written now and is checked against the URL
46
+ * being dialled. A bare string is the legacy shape, has no URL to check, and is still honoured —
47
+ * refusing it would silently unlink every repo that predates this change, which is a worse outage
48
+ * than the collision. Re-linking upgrades a repo to the safe shape.
49
+ */
50
+ export declare const credentialSlot: (url: string, projectId: string, orgId?: string) => string;
30
51
  /**
31
52
  * Resolve the cloud picture for a project rooted at `reticleRoot`. Reads the project's link file + the
32
53
  * user credential store; if the project isn't linked (no cloud.json), falls back to the global env creds
@@ -13,7 +13,8 @@ import { join } from 'node:path';
13
13
  import { ReticleDir } from '@reticlehq/core';
14
14
  import { resolveCloudConfig } from './cloud-sync.js';
15
15
  /** `<reticleRoot>/cloud.json` — the project's cloud binding + sync policy (non-secret). */
16
- export const CLOUD_LINK_FILE = 'cloud.json';
16
+ /** Re-exported so the existing importers keep working; the string itself lives in core. */
17
+ export const CLOUD_LINK_FILE = ReticleDir.CLOUD_LINK_FILE;
17
18
  /** `~/.reticle/credentials.json` — the user's per-cloud-project API keys (the only secret). */
18
19
  export const CREDENTIALS_FILE = 'credentials.json';
19
20
  const DEFAULT_SYNC_POLICY = { runs: true, memory: true, flows: true };
@@ -43,6 +44,7 @@ function parseLink(raw) {
43
44
  const sync = 'object' === typeof o.sync && o.sync !== null ? o.sync : {};
44
45
  return {
45
46
  projectId: o.projectId,
47
+ orgId: 'string' === typeof o.orgId && o.orgId.length > 0 ? o.orgId : undefined,
46
48
  url: o.url,
47
49
  sync: {
48
50
  runs: asBool(sync.runs, DEFAULT_SYNC_POLICY.runs),
@@ -53,12 +55,69 @@ function parseLink(raw) {
53
55
  };
54
56
  }
55
57
  /** Look up the API key for a cloud project id in the user credentials map. */
56
- function credentialFor(raw, projectId) {
58
+ /**
59
+ * The API key this machine holds for one project ON ONE CLOUD.
60
+ *
61
+ * The store used to be keyed by cloud project id ALONE, which collides in the case that is not rare
62
+ * at all — it is the default. `reticle link` names every project "default", so a repo linked to a
63
+ * self-hosted install and a repo linked to the hosted service both claim the slot `default`, and
64
+ * whichever was linked last wins. Measured: two repos on one machine, and the production key was
65
+ * being sent to a localhost server, which answered 401.
66
+ *
67
+ * That is the same class of defect as sending a session token to a host that did not issue it, and
68
+ * it deserves the same answer: a credential is scoped to the cloud that minted it, and no credential
69
+ * at all is better than one belonging to somewhere else.
70
+ *
71
+ * Two shapes are accepted. `{ key, url }` is what is written now and is checked against the URL
72
+ * being dialled. A bare string is the legacy shape, has no URL to check, and is still honoured —
73
+ * refusing it would silently unlink every repo that predates this change, which is a worse outage
74
+ * than the collision. Re-linking upgrades a repo to the safe shape.
75
+ */
76
+ export const credentialSlot = (url, projectId, orgId) => orgId === undefined || 0 === orgId.length
77
+ ? `${normalizeCloudUrl(url)}::${projectId}`
78
+ : `${normalizeCloudUrl(url)}::org::${orgId}::${projectId}`;
79
+ function credentialFor(raw, projectId, url, orgId) {
57
80
  if (typeof raw !== 'object' || null === raw)
58
81
  return null;
59
- const key = raw[projectId];
60
- return 'string' === typeof key && key.length > 0 ? key : null;
82
+ const store = raw;
83
+ /*
84
+ * The ORG slot first, because cloud + project is still not an identity.
85
+ *
86
+ * `link` names every project "default", so two ACCOUNTS on one cloud both claimed the slot
87
+ * `<url>::default` and the last link won. Measured on a laptop: a brand-new workspace ran
88
+ * `reticle login` and was handed a stored key belonging to a different organisation — valid, so
89
+ * every validation passed, and every run it pushed would have landed in a stranger's dashboard.
90
+ * Keying by cloud fixed two clouds colliding; only keying by ORG fixes two tenants colliding.
91
+ */
92
+ const orgScoped = orgId === undefined ? undefined : store[credentialSlot(url, projectId, orgId)];
93
+ if ('string' === typeof orgScoped && orgScoped.length > 0)
94
+ return orgScoped;
95
+ /*
96
+ * Then the cloud+project slot, which is what every repo linked before this change still holds. It
97
+ * is ambiguous between tenants by construction, which is why it is consulted second and why the
98
+ * writer above stopped producing it as the only entry.
99
+ */
100
+ const composite = store[credentialSlot(url, projectId)];
101
+ if ('string' === typeof composite && composite.length > 0)
102
+ return composite;
103
+ const entry = store[projectId];
104
+ if ('string' === typeof entry)
105
+ return entry.length > 0 ? entry : null;
106
+ if (typeof entry !== 'object' || null === entry)
107
+ return null;
108
+ const record = entry;
109
+ const key = record['key'];
110
+ const forUrl = record['url'];
111
+ if ('string' !== typeof key || 0 === key.length)
112
+ return null;
113
+ // A credential stamped with a DIFFERENT cloud is not this project's credential, however much the
114
+ // project ids match. Refuse rather than dial somewhere with somebody else's key.
115
+ if ('string' === typeof forUrl && normalizeCloudUrl(forUrl) !== normalizeCloudUrl(url))
116
+ return null;
117
+ return key;
61
118
  }
119
+ /** Trailing slashes are not identity: `https://x/` and `https://x` are one cloud. */
120
+ const normalizeCloudUrl = (url) => url.replace(/\/+$/, '');
62
121
  /**
63
122
  * Resolve the cloud picture for a project rooted at `reticleRoot`. Reads the project's link file + the
64
123
  * user credential store; if the project isn't linked (no cloud.json), falls back to the global env creds
@@ -80,7 +139,7 @@ export async function resolveProjectCloud(fs, reticleRoot, homeDir, env) {
80
139
  memory: link.sync.memory ?? DEFAULT_SYNC_POLICY.memory,
81
140
  flows: link.sync.flows ?? DEFAULT_SYNC_POLICY.flows,
82
141
  };
83
- const key = credentialFor(await readJson(fs, join(homeDir, ReticleDir.ROOT, CREDENTIALS_FILE)), link.projectId);
142
+ const key = credentialFor(await readJson(fs, join(homeDir, ReticleDir.ROOT, CREDENTIALS_FILE)), link.projectId, link.url, link.orgId);
84
143
  const config = key !== null ? { url: link.url.replace(/\/+$/, ''), apiKey: key } : null;
85
144
  return { config, policy, verify: link.verify, projectId: link.projectId };
86
145
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Optional cloud sync for saved flows. "Logged in" here means the two cloud env vars are set (written by
3
- * `reticle login` later; settable by hand today): the hosted URL and an API key from the Reticle Cloud
3
+ * `reticle login` later; settable by hand today): the hosted URL and an API key from the Reticle
4
4
  * dashboard. When present, a freshly-saved flow is pushed to `POST /v1/flows` so the team's regression
5
5
  * suite lives in the cloud — surviving refactors and runnable in CI. When absent, sync is a no-op and
6
6
  * everything stays 100% local (the "no phone-home" default: nothing leaves the machine unless you opt in).
@@ -115,21 +115,21 @@ declare const ServerVerificationSchema: z.ZodObject<{
115
115
  }>, "many">;
116
116
  summary: z.ZodString;
117
117
  }, "strip", z.ZodTypeAny, {
118
+ verdict: string | null;
118
119
  flows: {
119
120
  status: string;
120
121
  name: string;
121
122
  }[];
122
- verificationId: string;
123
- verdict: string | null;
124
123
  summary: string;
124
+ verificationId: string;
125
125
  }, {
126
+ verdict: string | null;
126
127
  flows: {
127
128
  status: string;
128
129
  name: string;
129
130
  }[];
130
- verificationId: string;
131
- verdict: string | null;
132
131
  summary: string;
132
+ verificationId: string;
133
133
  }>;
134
134
  export type ServerVerification = z.infer<typeof ServerVerificationSchema>;
135
135
  /**
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Optional cloud sync for saved flows. "Logged in" here means the two cloud env vars are set (written by
3
- * `reticle login` later; settable by hand today): the hosted URL and an API key from the Reticle Cloud
3
+ * `reticle login` later; settable by hand today): the hosted URL and an API key from the Reticle
4
4
  * dashboard. When present, a freshly-saved flow is pushed to `POST /v1/flows` so the team's regression
5
5
  * suite lives in the cloud — surviving refactors and runnable in CI. When absent, sync is a no-op and
6
6
  * everything stays 100% local (the "no phone-home" default: nothing leaves the machine unless you opt in).
@@ -44,18 +44,49 @@ const CLOUD_FETCH_TIMEOUT_MS = 30_000;
44
44
  export const CLOUD_VERIFY_TIMEOUT_MS = 120_000;
45
45
  /** Names Node gives an abort: `AbortSignal.timeout` raises TimeoutError, an explicit abort AbortError. */
46
46
  const TIMEOUT_ERROR_NAMES = new Set(['TimeoutError', 'AbortError']);
47
+ /**
48
+ * A header value `fetch` can actually send. Anything above U+00FF is not a valid ByteString.
49
+ *
50
+ * The one that happens in practice is an ellipsis. A dashboard masks a key as `rk_live_…` so it is
51
+ * never shown in full, somebody copies what they can SEE into `.env`, and the request dies deep
52
+ * inside undici with "Cannot convert argument to a ByteString because the character at index 15 has
53
+ * a value of 8230" — a byte offset into a header the user never wrote, naming no variable, no file
54
+ * and no fix.
55
+ */
56
+ const UNSENDABLE_HEADER_CHAR = /[^ -ÿ]/;
57
+ /** Which knob holds the credential behind a header, so the error can name the thing to go and edit. */
58
+ const CREDENTIAL_SOURCE = {
59
+ authorization: `${CloudEnv.KEY} (or the session from \`reticle login\`)`,
60
+ };
61
+ /**
62
+ * Refuse a request whose headers cannot be encoded, before it is sent.
63
+ *
64
+ * Before dialling on purpose: a credential that cannot be encoded cannot succeed, so the round trip
65
+ * buys only a slower and less specific failure.
66
+ */
67
+ function assertSendableHeaders(headers) {
68
+ for (const [name, value] of Object.entries(headers)) {
69
+ if (!UNSENDABLE_HEADER_CHAR.test(value))
70
+ continue;
71
+ const source = CREDENTIAL_SOURCE[name];
72
+ throw new Error(`the ${name} header carries a character that cannot be sent, which means the value is truncated — ` +
73
+ `a credential displayed masked as \`rk_live_…\` keeps that ellipsis when it is copied. ` +
74
+ `Paste the full value${source === undefined ? '' : ` into ${source}`}.`);
75
+ }
76
+ }
47
77
  /**
48
78
  * The fetch every cloud call goes through. Adds the abort signal, and turns the abort into a message
49
79
  * that says what happened and what to do — a bare `AbortError` reaching a user or an agent is a riddle,
50
80
  * and the agent-facing half of this product is judged on whether its errors are actionable.
51
81
  */
52
82
  export async function cloudFetch(url, init, timeoutMs = CLOUD_FETCH_TIMEOUT_MS) {
83
+ assertSendableHeaders(init.headers);
53
84
  try {
54
85
  return await fetch(url, { ...init, signal: AbortSignal.timeout(timeoutMs) });
55
86
  }
56
87
  catch (err) {
57
88
  if (err instanceof Error && TIMEOUT_ERROR_NAMES.has(err.name)) {
58
- throw new Error(`Reticle Cloud request timed out after ${Math.round(timeoutMs / 1000)}s: ${init.method} ${url}. ` +
89
+ throw new Error(`Reticle request timed out after ${Math.round(timeoutMs / 1000)}s: ${init.method} ${url}. ` +
59
90
  `The server accepted the connection but never answered. Check the network and ${CloudEnv.URL}, then retry — ` +
60
91
  `verification works locally without cloud.`);
61
92
  }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The derived records that ride along with runs, and the file each one lives in.
3
+ *
4
+ * A list rather than three hand-written blocks so adding a fourth is one line and cannot be
5
+ * half-done — the bundle, the hashing and the reporting all walk this.
6
+ */
7
+ export declare const DERIVED_RECORDS: readonly [{
8
+ readonly kind: "impact";
9
+ readonly file: "impact.json";
10
+ }, {
11
+ readonly kind: "flake";
12
+ readonly file: "flake.json";
13
+ }, {
14
+ readonly kind: "intent";
15
+ readonly file: "intent.json";
16
+ }];
17
+ export type DerivedKind = (typeof DERIVED_RECORDS)[number]['kind'];
18
+ /** What the machine reads from disk. Injected so a cycle is testable with no filesystem at all. */
19
+ export interface SyncSource {
20
+ /** Every run artifact currently on disk. */
21
+ runs: () => ReadonlyArray<{
22
+ runId: string;
23
+ payload: unknown;
24
+ }>;
25
+ /** Every saved flow. Small, upserted by name, so they ride along whenever anything else does. */
26
+ flows: () => readonly unknown[];
27
+ /** One derived record, or undefined when the file is absent. */
28
+ derived: (kind: DerivedKind) => unknown;
29
+ }
30
+ /** What the machine writes back. Separated from reads so a dry run is a source with no sink. */
31
+ export interface SyncSink {
32
+ /** Persist the triage decisions pulled from the dashboard. */
33
+ writeIssues: (issues: PulledIssues) => void;
34
+ /** Persist the cursor and the bookkeeping. */
35
+ writeState: (state: CloudSyncState) => void;
36
+ }
37
+ /** A decision a human made on the dashboard, as the machine stores it. */
38
+ export interface PulledIssue {
39
+ status: string;
40
+ flowName: string | null;
41
+ title: string;
42
+ at: number;
43
+ }
44
+ export interface PulledIssues {
45
+ /** Keyed by the server's fingerprint — stable across runs, which is the point of it. */
46
+ triage: Record<string, PulledIssue>;
47
+ }
48
+ /** `.reticle/cloud-state.json`. This machine's side of the conversation, never git-checked. */
49
+ export interface CloudSyncState {
50
+ /** Opaque. Handed back to the server verbatim; the machine must never parse or invent one. */
51
+ cursor?: string;
52
+ lastPushAt?: number;
53
+ lastPullAt?: number;
54
+ /** The last failure, kept so `reticle sync` can say why it is behind instead of just "0 sent". */
55
+ lastError?: string;
56
+ }
57
+ export interface SyncReport {
58
+ ok: boolean;
59
+ /** Runs the server accepted this cycle. */
60
+ runsSent: number;
61
+ /** Runs it refused, with the reason, so a bad artifact is visible rather than silently stuck. */
62
+ runsRejected: Array<{
63
+ index: number;
64
+ reason: string;
65
+ }>;
66
+ flowsSent: number;
67
+ /** Which derived records had actually moved. Empty on a quiet cycle, which is the normal case. */
68
+ derivedSent: DerivedKind[];
69
+ /** Decisions collected from the dashboard. */
70
+ pulled: number;
71
+ /** True when the pull page was full — call again now rather than waiting for the next tick. */
72
+ morePending: boolean;
73
+ /**
74
+ * The repo holds NO artifacts at all — no runs, no flows, no derived records.
75
+ *
76
+ * Distinct from "everything here is already pushed", which is the healthy steady state and looks
77
+ * identical from the outside. An empty repo usually means the app announces no projectId, so its
78
+ * runs are pooling into a different root and this binding will never report anything. Tracked so
79
+ * the summary can tell a user which of the two they are looking at.
80
+ */
81
+ localIsEmpty?: boolean;
82
+ /** Set when the cycle could not complete. The local record is untouched either way. */
83
+ error?: string;
84
+ }
85
+ export interface SyncDeps {
86
+ config: {
87
+ url: string;
88
+ apiKey: string;
89
+ };
90
+ source: SyncSource;
91
+ sink: SyncSink;
92
+ state: CloudSyncState;
93
+ now: () => number;
94
+ /** Injected so a cycle can be driven against a scripted server with no network. */
95
+ request: (url: string, init: {
96
+ method: string;
97
+ headers: Record<string, string>;
98
+ body?: string;
99
+ }) => Promise<{
100
+ status: number;
101
+ text: string;
102
+ }>;
103
+ }
104
+ /**
105
+ * Run one cycle. Never throws: the local record is already safe on disk, and a sync that can take
106
+ * down the thing it is backing up is worse than no sync.
107
+ */
108
+ export declare function runSyncCycle(deps: SyncDeps): Promise<SyncReport>;
109
+ /** One line a human can read, for the CLI and the daemon log. */
110
+ export declare function describeSync(report: SyncReport): string;