@reticlehq/server 3.3.0 → 3.5.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 (326) hide show
  1. package/LICENSE-ENTERPRISE +1 -1
  2. package/README.md +3 -3
  3. package/SKILL.md +12 -14
  4. package/dist/chromium-launch-options.d.ts +4 -3
  5. package/dist/chromium-launch-options.js +6 -2
  6. package/dist/command/cli/adhoc-suite.d.ts +22 -0
  7. package/dist/command/cli/adhoc-suite.js +90 -19
  8. package/dist/command/cli/adhoc-verdict.d.ts +35 -0
  9. package/dist/command/cli/adhoc-verdict.js +90 -3
  10. package/dist/command/cli/answers/gate-hook-message.d.ts +6 -0
  11. package/dist/command/cli/answers/gate-hook-message.js +6 -0
  12. package/dist/command/cli/auth/cloud-keystore.d.ts +0 -2
  13. package/dist/command/cli/auth/cloud-keystore.js +0 -3
  14. package/dist/command/cli/auth/cloud-session.d.ts +6 -2
  15. package/dist/command/cli/auth/cloud-session.js +2 -0
  16. package/dist/command/cli/auth/device-flow.d.ts +2 -2
  17. package/dist/command/cli/cli-doctor.js +7 -5
  18. package/dist/command/cli/cli-flow-commands.d.ts +3 -6
  19. package/dist/command/cli/cli-flow-commands.js +67 -4
  20. package/dist/command/cli/cli-parse.d.ts +8 -1
  21. package/dist/command/cli/cli-parse.js +24 -4
  22. package/dist/command/cli/cli-update-commands.js +8 -1
  23. package/dist/command/cli/cli-verify.d.ts +24 -3
  24. package/dist/command/cli/cli-verify.js +54 -11
  25. package/dist/command/cli/cloud-cli.js +64 -2
  26. package/dist/command/cli/cloud-kit.js +3 -5
  27. package/dist/command/cli/cloud-login.d.ts +1 -1
  28. package/dist/command/cli/cloud-login.js +27 -8
  29. package/dist/command/cli/demo/demo-page.js +6 -3
  30. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +5 -0
  31. package/dist/command/cli/doctor/browser/chromium-hint.js +5 -0
  32. package/dist/command/cli/launch/cli-launch.d.ts +8 -0
  33. package/dist/command/cli/launch/cli-launch.js +21 -9
  34. package/dist/command/cli/launch/sdk-version-match.d.ts +84 -0
  35. package/dist/command/cli/launch/sdk-version-match.js +206 -0
  36. package/dist/command/cli/lifecycle/daemon-lifecycle.js +2 -16
  37. package/dist/command/cli/ports/resolve/cli-port.d.ts +25 -0
  38. package/dist/command/cli/ports/resolve/cli-port.js +125 -15
  39. package/dist/command/cli/setup-mcp-cli.js +21 -13
  40. package/dist/command/cli/tutorial.d.ts +2 -2
  41. package/dist/command/cli/tutorial.js +21 -7
  42. package/dist/command/cli.js +79 -11
  43. package/dist/command/daemon/binding/daemon-status-probe.d.ts +14 -2
  44. package/dist/command/daemon/binding/daemon-status-probe.js +34 -6
  45. package/dist/command/daemon/daemon-resolve.d.ts +9 -0
  46. package/dist/command/daemon/daemon-resolve.js +1 -1
  47. package/dist/command/daemon/lifetime/daemon-usefulness.d.ts +7 -1
  48. package/dist/command/daemon/lifetime/daemon-usefulness.js +30 -7
  49. package/dist/command/dev/stale-issue-guard.d.ts +3 -4
  50. package/dist/command/dev/stale-issue-guard.js +8 -8
  51. package/dist/command/setup/bringup/connect-lease.d.ts +17 -0
  52. package/dist/command/setup/bringup/connect-lease.js +38 -0
  53. package/dist/command/setup/bringup/dev-server-files.d.ts +68 -0
  54. package/dist/command/setup/bringup/dev-server-files.js +131 -0
  55. package/dist/command/setup/bringup/dev-server-log-cap.d.ts +35 -0
  56. package/dist/command/setup/bringup/dev-server-log-cap.js +121 -0
  57. package/dist/command/setup/bringup/dev-server-wait.d.ts +2 -0
  58. package/dist/command/setup/bringup/dev-server-wait.js +1 -1
  59. package/dist/command/setup/bringup/ensure-daemon.d.ts +65 -4
  60. package/dist/command/setup/bringup/ensure-daemon.js +135 -16
  61. package/dist/command/setup/bringup/static-page-server.d.ts +35 -0
  62. package/dist/command/setup/bringup/static-page-server.js +151 -0
  63. package/dist/command/setup/desktop-shape.d.ts +4 -0
  64. package/dist/command/setup/desktop-shape.js +18 -2
  65. package/dist/command/setup/init/init-port.d.ts +15 -0
  66. package/dist/command/setup/init/init-port.js +44 -0
  67. package/dist/command/setup/init/init-runtime.d.ts +1 -1
  68. package/dist/command/setup/init/init-runtime.js +91 -9
  69. package/dist/command/setup/node-effects.d.ts +27 -14
  70. package/dist/command/setup/node-effects.js +86 -45
  71. package/dist/command/setup/probe/loopback-probe-urls.d.ts +7 -0
  72. package/dist/command/setup/probe/loopback-probe-urls.js +40 -0
  73. package/dist/command/setup/probe/page-probe.d.ts +13 -2
  74. package/dist/command/setup/probe/page-probe.js +18 -3
  75. package/dist/command/setup/run-setup.d.ts +28 -1
  76. package/dist/command/setup/run-setup.js +103 -15
  77. package/dist/command/setup/session-pick.d.ts +13 -0
  78. package/dist/command/setup/session-pick.js +20 -0
  79. package/dist/command/setup/setup-command.d.ts +2 -14
  80. package/dist/command/setup/setup-command.js +105 -31
  81. package/dist/command/setup/setup-mcp.d.ts +2 -0
  82. package/dist/command/setup/setup-mcp.js +17 -8
  83. package/dist/command/setup/terminal/wait-progress.d.ts +8 -0
  84. package/dist/command/setup/terminal/wait-progress.js +17 -0
  85. package/dist/command/version/peer-announce.d.ts +6 -2
  86. package/dist/command/version/peer-announce.js +8 -6
  87. package/dist/command/version/version-nudge.d.ts +17 -3
  88. package/dist/command/version/version-nudge.js +25 -4
  89. package/dist/features/crawl/crawl-empty.js +1 -1
  90. package/dist/features/crawl/crawl-tools.d.ts +6 -0
  91. package/dist/features/crawl/crawl-tools.js +57 -0
  92. package/dist/features/crawl/crawl.js +171 -157
  93. package/dist/features/exhaust/app-coverage.d.ts +33 -0
  94. package/dist/features/exhaust/app-coverage.js +31 -0
  95. package/dist/features/exhaust/code-coverage.d.ts +47 -0
  96. package/dist/features/exhaust/code-coverage.js +77 -0
  97. package/dist/features/exhaust/exhaust-run.d.ts +33 -0
  98. package/dist/features/exhaust/exhaust-run.js +82 -0
  99. package/dist/features/exhaust/explorer.d.ts +103 -0
  100. package/dist/features/exhaust/explorer.js +195 -0
  101. package/dist/features/exhaust/failure-branches.d.ts +33 -0
  102. package/dist/features/exhaust/failure-branches.js +92 -0
  103. package/dist/features/exhaust/ledger.d.ts +202 -0
  104. package/dist/features/exhaust/ledger.js +252 -0
  105. package/dist/features/exhaust/session-fold.d.ts +31 -0
  106. package/dist/features/exhaust/session-fold.js +79 -0
  107. package/dist/features/exhaust/session-port.d.ts +36 -0
  108. package/dist/features/exhaust/session-port.js +102 -0
  109. package/dist/features/harness/drive-report.d.ts +0 -9
  110. package/dist/features/harness/drive-report.js +18 -0
  111. package/dist/features/harness/driver.js +4 -7
  112. package/dist/features/harness/fill-values.d.ts +10 -0
  113. package/dist/features/harness/fill-values.js +50 -11
  114. package/dist/features/harness/jev-driver.js +30 -13
  115. package/dist/features/harness/openai-driver.d.ts +12 -0
  116. package/dist/features/harness/openai-driver.js +21 -12
  117. package/dist/features/harness/platform-config.js +5 -6
  118. package/dist/features/license/license.d.ts +2 -2
  119. package/dist/index.d.ts +9 -0
  120. package/dist/index.js +56 -19
  121. package/dist/judgement/capsule/capsule-store.d.ts +4 -4
  122. package/dist/judgement/domain/domain-model.js +3 -1
  123. package/dist/judgement/oracles/self-instrument.js +4 -2
  124. package/dist/judgement/runs/artifact/replay-mapping.d.ts +2 -2
  125. package/dist/judgement/runs/artifact/replay-mapping.js +44 -9
  126. package/dist/judgement/runs/verification-sync.d.ts +3 -1
  127. package/dist/judgement/runs/verification-sync.js +1 -1
  128. package/dist/language/flows/change/affected.d.ts +9 -0
  129. package/dist/language/flows/change/affected.js +35 -0
  130. package/dist/language/flows/change/gate.d.ts +20 -0
  131. package/dist/language/flows/change/gate.js +12 -1
  132. package/dist/language/flows/flow-author.d.ts +7 -0
  133. package/dist/language/flows/flow-author.js +19 -0
  134. package/dist/language/flows/flow-expect-grammar.js +1 -1
  135. package/dist/language/flows/flow-journey.d.ts +54 -0
  136. package/dist/language/flows/flow-journey.js +119 -0
  137. package/dist/language/flows/flow-replay-run.d.ts +2 -3
  138. package/dist/language/flows/flow-replay-run.js +13 -47
  139. package/dist/language/flows/flow-replay.d.ts +22 -8
  140. package/dist/language/flows/flow-replay.js +55 -9
  141. package/dist/language/flows/flow-result.d.ts +2 -0
  142. package/dist/language/flows/flow-result.js +25 -0
  143. package/dist/language/flows/flow-tools.js +17 -5
  144. package/dist/language/flows/flows.d.ts +10 -1
  145. package/dist/language/flows/flows.js +90 -14
  146. package/dist/language/flows/recording/replay-halt.js +3 -1
  147. package/dist/language/flows/recording/tape/recordings.d.ts +30 -1
  148. package/dist/language/flows/recording/tape/recordings.js +55 -1
  149. package/dist/language/flows/replay.d.ts +19 -0
  150. package/dist/language/flows/replay.js +42 -6
  151. package/dist/language/flows/verify-change-tools.js +5 -2
  152. package/dist/launch-chromium.d.ts +80 -0
  153. package/dist/launch-chromium.js +218 -0
  154. package/dist/memory/cloud/cloud-config.d.ts +11 -2
  155. package/dist/memory/cloud/cloud-config.js +47 -7
  156. package/dist/memory/cloud/cloud-sync.d.ts +4 -1
  157. package/dist/memory/cloud/cloud-sync.js +6 -9
  158. package/dist/memory/cloud/harness-offer.js +5 -6
  159. package/dist/memory/cloud/harness-switch.js +5 -6
  160. package/dist/memory/cloud/sync-cycle.d.ts +77 -3
  161. package/dist/memory/cloud/sync-cycle.js +325 -55
  162. package/dist/memory/cloud/sync-daemon.js +24 -20
  163. package/dist/memory/cloud/sync-disk.js +16 -2
  164. package/dist/memory/intent/intent-shard.d.ts +10 -32
  165. package/dist/memory/intent/intent-shard.js +0 -10
  166. package/dist/memory/intent/intent-tools.js +8 -3
  167. package/dist/memory/journal/drive-flow.d.ts +18 -13
  168. package/dist/memory/journal/drive-flow.js +45 -37
  169. package/dist/memory/journal/envelope.d.ts +8 -8
  170. package/dist/memory/journal/journal-query.d.ts +14 -0
  171. package/dist/memory/journal/journal-query.js +15 -0
  172. package/dist/memory/journal/on-disk/workspace-tiers.js +2 -0
  173. package/dist/memory/journal/session-end.d.ts +9 -1
  174. package/dist/memory/journal/session-end.js +24 -12
  175. package/dist/memory/project/artifact-root-resolver.d.ts +10 -0
  176. package/dist/memory/project/artifact-root-resolver.js +46 -19
  177. package/dist/memory/project/dir/reticle-dir.d.ts +2 -0
  178. package/dist/memory/project/dir/reticle-dir.js +1 -0
  179. package/dist/memory/project/project-tools.js +3 -1
  180. package/dist/portal/bridge/auth-failure-reason.d.ts +1 -1
  181. package/dist/portal/bridge/auth-failure-reason.js +4 -2
  182. package/dist/portal/bridge/bridge.d.ts +12 -3
  183. package/dist/portal/bridge/bridge.js +49 -3
  184. package/dist/portal/input/js-coverage.d.ts +25 -0
  185. package/dist/portal/input/js-coverage.js +30 -0
  186. package/dist/portal/input/real-input.d.ts +11 -0
  187. package/dist/portal/input/real-input.js +29 -4
  188. package/dist/portal/input/viewport-tools.js +16 -6
  189. package/dist/portal/pool/playwright-launcher.d.ts +6 -0
  190. package/dist/portal/pool/playwright-launcher.js +20 -13
  191. package/dist/portal/session/dev-server/dev-command.d.ts +2 -2
  192. package/dist/portal/session/dev-server/dev-command.js +30 -12
  193. package/dist/portal/session/dev-server/dev-server-probe.js +17 -1
  194. package/dist/portal/session/dev-server/route-status-probe.d.ts +24 -0
  195. package/dist/portal/session/dev-server/route-status-probe.js +57 -0
  196. package/dist/portal/session/facts/observed-state.d.ts +11 -0
  197. package/dist/portal/session/facts/observed-state.js +34 -0
  198. package/dist/portal/session/no-session-diagnosis.d.ts +29 -0
  199. package/dist/portal/session/no-session-diagnosis.js +62 -5
  200. package/dist/portal/session/no-session-next-action.d.ts +13 -0
  201. package/dist/portal/session/no-session-next-action.js +48 -9
  202. package/dist/portal/session/no-session-watch.d.ts +18 -0
  203. package/dist/portal/session/no-session-watch.js +88 -2
  204. package/dist/portal/session/session-health.js +3 -2
  205. package/dist/portal/session/session-manager.d.ts +20 -6
  206. package/dist/portal/session/session-manager.js +19 -3
  207. package/dist/portal/session/session-reaper.d.ts +28 -0
  208. package/dist/portal/session/session-reaper.js +49 -1
  209. package/dist/portal/session/session.d.ts +10 -8
  210. package/dist/portal/session/session.js +24 -32
  211. package/dist/surface/http-server.d.ts +3 -2
  212. package/dist/surface/http-server.js +8 -5
  213. package/dist/surface/mcp/mcp-proxy.d.ts +2 -2
  214. package/dist/surface/mcp/mcp-proxy.js +2 -2
  215. package/dist/surface/mcp/mcp.js +5 -3
  216. package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +13 -1
  217. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +27 -2
  218. package/dist/surface/mcp/proxy/proxy-log.js +3 -5
  219. package/dist/surface/mcp/server-instructions.js +9 -7
  220. package/dist/surface/tools/act/after-match.d.ts +17 -0
  221. package/dist/surface/tools/act/after-match.js +30 -0
  222. package/dist/surface/tools/act/durable.d.ts +69 -0
  223. package/dist/surface/tools/act/durable.js +139 -0
  224. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +13 -6
  225. package/dist/surface/tools/act/navigation/navigate-arrival.js +47 -10
  226. package/dist/surface/tools/act/navigation/navigate-result.d.ts +1 -12
  227. package/dist/surface/tools/act/navigation/navigate-result.js +21 -3
  228. package/dist/surface/tools/act/proved-controls.d.ts +9 -0
  229. package/dist/surface/tools/act/proved-controls.js +11 -0
  230. package/dist/surface/tools/act/sequence-grade.d.ts +5 -0
  231. package/dist/surface/tools/act/sequence-grade.js +32 -1
  232. package/dist/surface/tools/act-capsule.d.ts +7 -0
  233. package/dist/surface/tools/act-capsule.js +46 -1
  234. package/dist/surface/tools/act-sequence-tool.js +25 -8
  235. package/dist/surface/tools/act-tools.js +28 -52
  236. package/dist/surface/tools/assert/assert-grade.js +12 -1
  237. package/dist/surface/tools/assert/assert-verdict.js +2 -2
  238. package/dist/surface/tools/browser-tools.js +14 -2
  239. package/dist/surface/tools/coverage-tools.js +27 -1
  240. package/dist/surface/tools/dynamic-tools.js +29 -2
  241. package/dist/surface/tools/error-recovery.js +5 -6
  242. package/dist/surface/tools/feedback-invite.js +6 -4
  243. package/dist/surface/tools/harness-explore.d.ts +3 -2
  244. package/dist/surface/tools/harness-explore.js +12 -7
  245. package/dist/surface/tools/invoke-tool.js +12 -8
  246. package/dist/surface/tools/lease-tools.js +15 -6
  247. package/dist/surface/tools/live-call-text.js +4 -1
  248. package/dist/surface/tools/observe-tools.js +9 -1
  249. package/dist/surface/tools/tool-kit.d.ts +10 -0
  250. package/dist/surface/tools/tools.js +4 -0
  251. package/dist/telemetry/argument-shape.d.ts +0 -2
  252. package/dist/telemetry/argument-shape.js +1 -5
  253. package/dist/telemetry/error-class.d.ts +2 -3
  254. package/dist/telemetry/error-class.js +2 -3
  255. package/dist/telemetry/harness-drive.d.ts +0 -2
  256. package/dist/telemetry/harness-drive.js +0 -4
  257. package/dist/telemetry/onboarding-firsts.d.ts +2 -2
  258. package/dist/telemetry/onboarding-firsts.js +2 -2
  259. package/dist/wire-journal.d.ts +3 -0
  260. package/dist/wire-journal.js +4 -0
  261. package/dist/workspace-packages.d.ts +0 -2
  262. package/dist/workspace-packages.js +1 -9
  263. package/docs/README.md +4 -2
  264. package/docs/actions.mdx +3 -4
  265. package/docs/agent-cheatsheet.md +4 -4
  266. package/docs/architecture.md +6 -6
  267. package/docs/best-practices.mdx +1 -1
  268. package/docs/capabilities.mdx +32 -1
  269. package/docs/cli/cloud.mdx +20 -8
  270. package/docs/cli/doctor.mdx +4 -0
  271. package/docs/cli/feedback.mdx +2 -2
  272. package/docs/cli/gate.mdx +9 -4
  273. package/docs/cli/update.mdx +10 -0
  274. package/docs/cli.mdx +22 -7
  275. package/docs/desktop.mdx +14 -10
  276. package/docs/environment.mdx +87 -0
  277. package/docs/faq.mdx +18 -16
  278. package/docs/first-drive.md +5 -13
  279. package/docs/flows.md +21 -2
  280. package/docs/frameworks.mdx +29 -10
  281. package/docs/getting-started.md +15 -5
  282. package/docs/http-transport.md +6 -3
  283. package/docs/human-control.md +2 -2
  284. package/docs/index.mdx +16 -6
  285. package/docs/install-agentic.mdx +2 -2
  286. package/docs/install-manual.mdx +1 -1
  287. package/docs/integration-patterns.md +3 -3
  288. package/docs/multi-agent-testing.md +1 -1
  289. package/docs/packages/babel-plugin.mdx +1 -1
  290. package/docs/packages/browser.mdx +2 -2
  291. package/docs/packages/next.mdx +3 -0
  292. package/docs/packages/react.mdx +2 -2
  293. package/docs/packages/tauri.mdx +1 -1
  294. package/docs/packages/vite-plugin.mdx +4 -2
  295. package/docs/packages.mdx +9 -9
  296. package/docs/predicates.mdx +50 -5
  297. package/docs/quickstart.mdx +12 -10
  298. package/docs/recipes.mdx +2 -2
  299. package/docs/state-management.mdx +2 -2
  300. package/docs/system-map.md +1 -1
  301. package/docs/telemetry.md +1 -1
  302. package/docs/token-efficiency.md +3 -3
  303. package/docs/tools/act-and-wait.mdx +1 -0
  304. package/docs/tools/act-sequence.mdx +1 -1
  305. package/docs/tools/act.mdx +1 -1
  306. package/docs/tools/console.mdx +1 -1
  307. package/docs/tools/inspect.mdx +1 -1
  308. package/docs/tools/network.mdx +1 -1
  309. package/docs/tools/query.mdx +1 -1
  310. package/docs/tools/sessions.mdx +1 -1
  311. package/docs/tools/snapshot.mdx +1 -1
  312. package/docs/tools/state.mdx +1 -1
  313. package/docs/tools/wait-for.mdx +1 -1
  314. package/docs/usage.md +7 -7
  315. package/docs/use-cases.mdx +157 -0
  316. package/docs/vs-screenshots.mdx +3 -3
  317. package/docs/what-is-recorded.md +2 -2
  318. package/docs/what-is-reticle.mdx +17 -20
  319. package/docs/why-reticle.mdx +14 -21
  320. package/package.json +10 -9
  321. package/dist/judgement/capsule/minimize.d.ts +0 -12
  322. package/dist/judgement/capsule/minimize.js +0 -22
  323. package/dist/judgement/phenomena/phenomena.d.ts +0 -27
  324. package/dist/judgement/phenomena/phenomena.js +0 -101
  325. package/dist/language/flows/flow-report.d.ts +0 -22
  326. package/dist/language/flows/flow-report.js +0 -106
@@ -37,17 +37,36 @@ const LoginSchema = z.object({
37
37
  * code out of a server log they may not even be tailing.
38
38
  */
39
39
  const RequestCodeSchema = z.object({ devCode: z.string().optional() });
40
+ const MeSchema = z.object({ email: z.string().min(1).nullable().optional() });
41
+ /** The signed-in address, asked of the cloud. Best-effort: a login never fails for want of it. */
42
+ const signedInEmail = async (url, token) => {
43
+ try {
44
+ return MeSchema.parse(await api('GET', `${url}/v1/me`, token)).email ?? undefined;
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ };
40
50
  /** Persist a session token under ~/.reticle and print the next step. Shared by both login paths. */
41
- const writeSession = async (url, token, orgName, orgId, project, link) => {
51
+ const writeSession = async (url, token, orgName, orgId, project, link, email, autoLink = true) => {
42
52
  await mkdir(join(home(), SESSIONS_DIR), { recursive: true });
43
- const body = `${JSON.stringify({ url: normalizeUrl(url), token, orgName, ...(orgId === undefined ? {} : { orgId }) }, null, 2)}\n`;
53
+ // Who signed in, so a flow saved on this machine can say which person made it (FlowFile.author).
54
+ const who = email ?? (await signedInEmail(url, token));
55
+ const body = `${JSON.stringify({
56
+ url: normalizeUrl(url),
57
+ token,
58
+ orgName,
59
+ ...(orgId === undefined ? {} : { orgId }),
60
+ ...(who === undefined ? {} : { email: who }),
61
+ }, null, 2)}\n`;
44
62
  // Both, on purpose. The per-host file is what lets another environment stay logged in; the active
45
63
  // file is what a bare command with no override resolves through, and keeping it means nothing
46
64
  // about the single-environment workflow changes.
47
65
  await writeFile(sessionPath(home(), url), body);
48
66
  await writeFile(join(home(), SESSION_FILE), body);
49
67
  emit({ loggedIn: orgName, session: join(home(), SESSION_FILE) });
50
- await linkAfterLogin(url, project, link);
68
+ if (autoLink)
69
+ await linkAfterLogin(url, project, link);
51
70
  };
52
71
  /**
53
72
  * Finish the job when the shell is already standing in an unlinked Reticle project.
@@ -106,7 +125,7 @@ const linkAfterLogin = async (url, project, link) => {
106
125
  * Browser device flow — the DEFAULT `reticle login` (like `gh auth login`): fetch a device + user code,
107
126
  * open the browser to approve, then poll until the user confirms. No email to type, no code to copy back.
108
127
  */
109
- const cmdLoginDevice = async (explicitUrl, project, link) => {
128
+ const cmdLoginDevice = async (explicitUrl, project, link, autoLink) => {
110
129
  const url = baseUrl(null, explicitUrl);
111
130
  const started = DeviceStartSchema.parse(await api('POST', `${url}/v1/auth/device/start`, null, {}));
112
131
  hint(`Opening ${started.verificationUri} — confirm this code in the browser: ${started.userCode}`);
@@ -116,7 +135,7 @@ const cmdLoginDevice = async (explicitUrl, project, link) => {
116
135
  await sleep(intervalMs);
117
136
  const poll = DevicePollSchema.parse(await api('POST', `${url}/v1/auth/device/token`, null, { deviceCode: started.deviceCode }));
118
137
  if ('approved' === poll.status && poll.token !== undefined && poll.org !== undefined) {
119
- await writeSession(url, poll.token, poll.org.name, poll.org.id, project, link);
138
+ await writeSession(url, poll.token, poll.org.name, poll.org.id, project, link, undefined, autoLink);
120
139
  return 0;
121
140
  }
122
141
  if ('pending' === poll.status) {
@@ -136,12 +155,12 @@ const cmdLoginDevice = async (explicitUrl, project, link) => {
136
155
  * `reticle login` — browser device flow by default; `--email <e>` (or a positional email) keeps the
137
156
  * headless two-step code path for CI/servers where opening a browser makes no sense.
138
157
  */
139
- export const cmdLogin = async (argv, link) => {
158
+ export const cmdLogin = async (argv, link, autoLink = true) => {
140
159
  const f = flags(argv);
141
160
  const positional = argv[0] !== undefined && !argv[0].startsWith('--') ? argv[0] : undefined;
142
161
  const email = f['email'] ?? positional;
143
162
  if (email === undefined)
144
- return cmdLoginDevice(f['url'], f['project'], link);
163
+ return cmdLoginDevice(f['url'], f['project'], link, autoLink);
145
164
  const org = f['org'];
146
165
  const url = baseUrl(null, f['url']);
147
166
  let code = f['code'];
@@ -159,7 +178,7 @@ export const cmdLogin = async (argv, link) => {
159
178
  code = requested.devCode;
160
179
  }
161
180
  const parsed = LoginSchema.parse(await api('POST', `${url}/v1/auth/login`, null, { email, code }));
162
- await writeSession(url, parsed.token, parsed.org.name, parsed.org.id, f['project'], link);
181
+ await writeSession(url, parsed.token, parsed.org.name, parsed.org.id, f['project'], link, email, autoLink);
163
182
  return 0;
164
183
  };
165
184
  /** `reticle logout` — forget the cached session token (per-project keys under credentials.json stay). */
@@ -67,16 +67,19 @@ const HTML_CONTENT_TYPE = 'text/html; charset=utf-8';
67
67
  function bareSpecifiersIn(dir) {
68
68
  const found = new Set();
69
69
  const walk = (at) => {
70
- for (const name of readdirSync(at)) {
70
+ for (const entry of readdirSync(at, { withFileTypes: true })) {
71
+ const name = entry.name;
71
72
  const path = join(at, name);
72
- if (statSync(path).isDirectory()) {
73
+ if (entry.isDirectory()) {
73
74
  if ('node_modules' !== name)
74
75
  walk(path);
75
76
  continue;
76
77
  }
77
78
  if (!name.endsWith('.js') || name.endsWith('.test.js'))
78
79
  continue;
79
- for (const match of readFileSync(path, 'utf8').matchAll(/from\s*'([^.'][^']*)'/g)) {
80
+ // `from` as a keyword, not the tail of a string: core's `z.discriminatedUnion('from', [...])`
81
+ // read as an import of everything up to the next quote. A specifier never holds whitespace.
82
+ for (const match of readFileSync(path, 'utf8').matchAll(/(?<![\w$.'"])from\s*'([^.'\s][^'\s]*)'/g)) {
80
83
  const spec = match[1];
81
84
  if (spec !== undefined && !NOT_SERVABLE.test(spec))
82
85
  found.add(spec);
@@ -43,6 +43,11 @@ export interface ChromiumProbe {
43
43
  * and kept reading "not installed", with no way to see that neither was ever searched.
44
44
  */
45
45
  searchedPaths?: readonly string[] | undefined;
46
+ /**
47
+ * The installed browser Reticle drives instead when its own build is absent (`Google Chrome`,
48
+ * `Microsoft Edge`) — see launch-chromium. Present only when `exists` is false and one was found.
49
+ */
50
+ fallback?: string | undefined;
46
51
  }
47
52
  /** The install command, pinned when we know what to pin to. */
48
53
  export declare function chromiumInstallCommand(playwrightVersion?: string): string;
@@ -170,6 +170,11 @@ export function chromiumHint(probe) {
170
170
  : `✓ installed (${probe.wantedRevision})`;
171
171
  }
172
172
  const command = chromiumInstallCommand(probe.playwrightVersion);
173
+ // Not a failure: every Reticle browser launches on this one. The install command stays on the
174
+ // line for a reader who wants the exact build the tests run against.
175
+ if (probe.fallback !== undefined && probe.executablePath !== undefined) {
176
+ return `✓ using the installed ${probe.fallback} (Playwright's own Chromium is not installed; ${command} adds it)`;
177
+ }
173
178
  if (probe.executablePath === undefined) {
174
179
  // Same rule as the missing-browser line below: a verdict with nowhere to check it against reads
175
180
  // as a broken check. The roots say whether this process even searched the directory the reader
@@ -12,6 +12,13 @@ export { summarizeStatus } from '../../daemon/binding/daemon-status-probe.js';
12
12
  * another agent's session on a version bump, which is worse than a loud line.
13
13
  */
14
14
  export declare function warnOnDaemonSkew(port: number): Promise<void>;
15
+ /**
16
+ * The skew sentence for a daemon's `/status` body, or undefined when it speaks our contract.
17
+ *
18
+ * Shared with `init`, which must not ADOPT a skewed daemon: an older one refuses every hello from
19
+ * the page init just instrumented.
20
+ */
21
+ export declare function daemonSkew(status: unknown): string | undefined;
15
22
  /** What `reticle open` should do: reuse an already-connected tab, open a new one, or ask for a url. */
16
23
  type OpenDecision = {
17
24
  action: 'reuse';
@@ -48,6 +55,7 @@ export declare function decideOpen(all: {
48
55
  url: string;
49
56
  unresponsive?: boolean;
50
57
  hidden?: boolean;
58
+ lastSeenMs?: number;
51
59
  }[], url: string | undefined): OpenDecision;
52
60
  /** The OS command that opens a URL in the default browser, per platform. Pure — unit-tested. */
53
61
  export declare function openCommand(url: string, platform: NodeJS.Platform): {
@@ -1,6 +1,6 @@
1
1
  import { NodePlatform } from '../../../machine/platform.js';
2
2
  import { spawn } from 'node:child_process';
3
- import { isOpaqueOrigin } from '@reticlehq/core';
3
+ import { isOpaqueOrigin, SESSION_HEALTH } from '@reticlehq/core';
4
4
  import { daemonFix, describeSkew } from '../../version/version-skew.js';
5
5
  import { CONTRACT_FINGERPRINT } from '@reticlehq/core';
6
6
  import { SERVER_VERSION } from '../../version/identity/server-version.js';
@@ -29,17 +29,25 @@ function statusField(payload, key) {
29
29
  * another agent's session on a version bump, which is worse than a loud line.
30
30
  */
31
31
  export async function warnOnDaemonSkew(port) {
32
- const status = await fetchStatus(port);
32
+ const skew = daemonSkew(await fetchStatus(port));
33
+ if (skew !== undefined)
34
+ log('reticle_daemon_skew', { port, warning: skew });
35
+ }
36
+ /**
37
+ * The skew sentence for a daemon's `/status` body, or undefined when it speaks our contract.
38
+ *
39
+ * Shared with `init`, which must not ADOPT a skewed daemon: an older one refuses every hello from
40
+ * the page init just instrumented.
41
+ */
42
+ export function daemonSkew(status) {
33
43
  const daemonVersion = statusField(status, 'version');
34
- const skew = describeSkew({
44
+ return describeSkew({
35
45
  what: 'the daemon already running on this port',
36
46
  version: daemonVersion,
37
47
  contract: statusField(status, 'contract'),
38
48
  // The daemon is the peer here and this process is the agent side.
39
49
  fix: daemonFix(daemonVersion, SERVER_VERSION),
40
50
  }, { version: SERVER_VERSION, contract: CONTRACT_FINGERPRINT });
41
- if (skew !== undefined)
42
- log('reticle_daemon_skew', { port, warning: skew });
43
51
  }
44
52
  function sameOrigin(a, b) {
45
53
  try {
@@ -82,10 +90,14 @@ export function decideOpen(all, url) {
82
90
  // three checks that could never resolve. Opening the url instead costs one foreground tab and the
83
91
  // hidden one is left exactly where it was — nothing is navigated out from under anybody.
84
92
  //
85
- // HIDDEN, not `throttled`: throttled is `hidden || stale`, and a stale-but-visible tab is usually
86
- // a quiet page that drives fine. Excluding those would pile up a duplicate tab per run, which is
87
- // the thing the origin match below exists to prevent.
88
- const sessions = all.filter((s) => true !== s.unresponsive && true !== s.hidden);
93
+ // And a tab the SDK has gone SILENT in. The page heartbeats on a native timer every few seconds
94
+ // whatever it is doing, so a quiet page is not a silent one: past the daemon's own staleness
95
+ // threshold the page is frozen (a discarded tab, a sleeping machine, a wedged main thread). This
96
+ // looked only at the url, and `verify --expect` drove a tab last heard from 105s earlier into
97
+ // "command 'match' timed out after 8000ms" instead of opening the url it had been given.
98
+ const sessions = all.filter((s) => true !== s.unresponsive &&
99
+ true !== s.hidden &&
100
+ (s.lastSeenMs ?? 0) <= SESSION_HEALTH.STALE_THRESHOLD_MS);
89
101
  if (url === undefined) {
90
102
  const first = sessions[0];
91
103
  if (first !== undefined)
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Run the CLI whose major matches the SDK the project actually installed.
3
+ *
4
+ * The agent's MCP entry and every rule file `init` writes say `npx @reticlehq/server`, deliberately
5
+ * unpinned (see `npxServerArgs` in init), which resolves the LATEST server — while the project's SDK
6
+ * stays locked at whatever `init` installed. A project wired on the previous major therefore got a
7
+ * daemon from the next one, and every verdict came back `version_skew`; a global install from an
8
+ * older release shadows npx the same way in the other direction. Nothing the project could do fixed
9
+ * it short of upgrading, and the report named the skew without resolving it.
10
+ *
11
+ * So the one entry every command passes through asks, before doing anything: is the SDK in this
12
+ * project on my major? If not, it hands the same arguments to `npx @reticlehq/server@<that version>`
13
+ * and steps aside. Only across majors — a minor apart verifies fine, and re-running for it would
14
+ * cost an npx resolve on every command for nothing.
15
+ *
16
+ * Deliberately free of `@/` and relative imports: the stdio test runs this file in a bare Node child
17
+ * to see the real file descriptors, and that child has neither the alias nor the build.
18
+ */
19
+ import { type ChildProcess } from 'node:child_process';
20
+ /** The packages an instrumented project installs; they release in lockstep, so any one will do. */
21
+ export declare const SDK_PACKAGES: readonly ["@reticlehq/react", "@reticlehq/browser", "@reticlehq/next", "@reticlehq/vite-plugin"];
22
+ export declare const VersionMatchEnv: {
23
+ /** Set to anything to keep the CLI you ran, whatever the project installed. */
24
+ readonly OPT_OUT: "RETICLE_NO_VERSION_MATCH";
25
+ /** Set on the re-run child, so it can never re-run itself again. */
26
+ readonly REEXECUTED: "RETICLE_VERSION_MATCHED";
27
+ /** What npm puts here for `npx <spec>` — how a version typed on the npx line is recognised. */
28
+ readonly NPX_PACKAGE: "npm_config_package";
29
+ };
30
+ /**
31
+ * The SDK version installed for `projectDir`, found the way Node resolves it: the nearest
32
+ * `node_modules` at or above the directory, so a package hoisted to a workspace root counts.
33
+ */
34
+ export declare function installedSdkVersion(projectDir: string, readFile: (path: string) => string | undefined): string | undefined;
35
+ /** `readFile` for the real filesystem: the text, or undefined when there is nothing readable. */
36
+ export declare function readTextFile(path: string): string | undefined;
37
+ /** The one line a person sees on stderr when the CLI hands over — stdout belongs to the protocol. */
38
+ export declare function versionMatchNote(sdkVersion: string, cliVersion: string): string;
39
+ export interface VersionMatchInput {
40
+ argv: readonly string[];
41
+ cliVersion: string;
42
+ env: Readonly<Record<string, string | undefined>>;
43
+ projectDir: string;
44
+ readFile: (path: string) => string | undefined;
45
+ }
46
+ /**
47
+ * The version to re-run at, or undefined to carry on in this process. Pure: the filesystem and the
48
+ * environment are handed in.
49
+ */
50
+ export declare function versionToMatch(input: VersionMatchInput): string | undefined;
51
+ /** How to launch `npx --yes @reticlehq/server@<version> <args>` on this platform. */
52
+ export declare function npxInvocation(version: string, args: readonly string[], on: string): {
53
+ file: string;
54
+ argv: string[];
55
+ shell: boolean;
56
+ };
57
+ /** The two things the re-run needs from its child: when it ends, and a way to pass a signal on. */
58
+ export interface ReexecChild {
59
+ /** Called once, with the exit code — or undefined when the child never started or was killed. */
60
+ onEnd(listener: (code: number | undefined, error: Error | undefined) => void): void;
61
+ kill(signal: NodeJS.Signals): void;
62
+ }
63
+ export type ReexecSpawner = (file: string, argv: string[], options: {
64
+ stdio: 'inherit';
65
+ env: NodeJS.ProcessEnv;
66
+ shell: boolean;
67
+ }) => ReexecChild;
68
+ /** A ChildProcess, seen through `ReexecChild`. */
69
+ export declare function asReexecChild(child: ChildProcess): ReexecChild;
70
+ /** The real spawner. */
71
+ export declare const nodeSpawner: ReexecSpawner;
72
+ /**
73
+ * Hand this invocation to the matching version and exit with its code.
74
+ *
75
+ * `stdio: 'inherit'` gives the child this process's own file descriptors, so an MCP client's JSON-RPC
76
+ * stream reaches it without passing through a byte of JavaScript here — nothing to buffer, reframe or
77
+ * lose. For that to hold, this must run before anything writes to stdout.
78
+ */
79
+ export declare function reexecAtVersion(version: string, args: readonly string[], env: NodeJS.ProcessEnv, deps: {
80
+ spawn: ReexecSpawner;
81
+ exit: (code: number) => void;
82
+ platform?: string;
83
+ warn?: (line: string) => void;
84
+ }): void;
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Run the CLI whose major matches the SDK the project actually installed.
3
+ *
4
+ * The agent's MCP entry and every rule file `init` writes say `npx @reticlehq/server`, deliberately
5
+ * unpinned (see `npxServerArgs` in init), which resolves the LATEST server — while the project's SDK
6
+ * stays locked at whatever `init` installed. A project wired on the previous major therefore got a
7
+ * daemon from the next one, and every verdict came back `version_skew`; a global install from an
8
+ * older release shadows npx the same way in the other direction. Nothing the project could do fixed
9
+ * it short of upgrading, and the report named the skew without resolving it.
10
+ *
11
+ * So the one entry every command passes through asks, before doing anything: is the SDK in this
12
+ * project on my major? If not, it hands the same arguments to `npx @reticlehq/server@<that version>`
13
+ * and steps aside. Only across majors — a minor apart verifies fine, and re-running for it would
14
+ * cost an npx resolve on every command for nothing.
15
+ *
16
+ * Deliberately free of `@/` and relative imports: the stdio test runs this file in a bare Node child
17
+ * to see the real file descriptors, and that child has neither the alias nor the build.
18
+ */
19
+ import { spawn } from 'node:child_process';
20
+ import { readFileSync } from 'node:fs';
21
+ import { dirname, join } from 'node:path';
22
+ import { NodePlatform, windowsShellArg } from '@reticlehq/init';
23
+ /** The packages an instrumented project installs; they release in lockstep, so any one will do. */
24
+ export const SDK_PACKAGES = [
25
+ '@reticlehq/react',
26
+ '@reticlehq/browser',
27
+ '@reticlehq/next',
28
+ '@reticlehq/vite-plugin',
29
+ ];
30
+ /** The package that carries the `reticle` bin — what the re-run asks npx for. */
31
+ const SERVER_PACKAGE = '@reticlehq/server';
32
+ const NPX = 'npx';
33
+ const NPX_WINDOWS = 'npx.cmd';
34
+ const NPX_YES = '--yes';
35
+ const NODE_MODULES = 'node_modules';
36
+ const PACKAGE_JSON = 'package.json';
37
+ const LATEST_TAG = 'latest';
38
+ const MARKED = '1';
39
+ export const VersionMatchEnv = {
40
+ /** Set to anything to keep the CLI you ran, whatever the project installed. */
41
+ OPT_OUT: 'RETICLE_NO_VERSION_MATCH',
42
+ /** Set on the re-run child, so it can never re-run itself again. */
43
+ REEXECUTED: 'RETICLE_VERSION_MATCHED',
44
+ /** What npm puts here for `npx <spec>` — how a version typed on the npx line is recognised. */
45
+ NPX_PACKAGE: 'npm_config_package',
46
+ };
47
+ /**
48
+ * Commands whose job is to CHANGE the version, or to report on this binary. Re-running `init` or
49
+ * `update` at the project's old version would undo exactly what the person asked for; `version` and
50
+ * `help` describe the binary they ran. `_daemon` is spawned by a CLI that already decided.
51
+ */
52
+ const KEEP_COMMANDS = new Set([
53
+ 'init',
54
+ 'update',
55
+ 'rollback',
56
+ 'version',
57
+ 'help',
58
+ '_daemon',
59
+ ]);
60
+ const KEEP_FLAGS = new Set(['--version', '-v', '--help', '-h']);
61
+ const SETUP_COMMAND = 'setup';
62
+ const SETUP_INSTALL = 'install';
63
+ /** A published release — not `workspace:*`, a link, or a tag npx could not pin. */
64
+ const RELEASE_VERSION = /^(\d+)\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
65
+ function majorOf(version) {
66
+ return RELEASE_VERSION.exec(version)?.[1];
67
+ }
68
+ /**
69
+ * The SDK version installed for `projectDir`, found the way Node resolves it: the nearest
70
+ * `node_modules` at or above the directory, so a package hoisted to a workspace root counts.
71
+ */
72
+ export function installedSdkVersion(projectDir, readFile) {
73
+ let dir = projectDir;
74
+ for (;;) {
75
+ for (const pkg of SDK_PACKAGES) {
76
+ const raw = readFile(join(dir, NODE_MODULES, pkg, PACKAGE_JSON));
77
+ if (raw === undefined)
78
+ continue;
79
+ try {
80
+ const version = JSON.parse(raw).version;
81
+ if ('string' === typeof version)
82
+ return version;
83
+ }
84
+ catch {
85
+ // A manifest that will not parse is no evidence; try the next package.
86
+ }
87
+ }
88
+ const parent = dirname(dir);
89
+ if (parent === dir)
90
+ return undefined;
91
+ dir = parent;
92
+ }
93
+ }
94
+ /** `readFile` for the real filesystem: the text, or undefined when there is nothing readable. */
95
+ export function readTextFile(path) {
96
+ try {
97
+ return readFileSync(path, 'utf8');
98
+ }
99
+ catch {
100
+ return undefined;
101
+ }
102
+ }
103
+ /** The one line a person sees on stderr when the CLI hands over — stdout belongs to the protocol. */
104
+ export function versionMatchNote(sdkVersion, cliVersion) {
105
+ return (`This project's Reticle SDK is ${sdkVersion}; running ${SERVER_PACKAGE}@${sdkVersion} to match ` +
106
+ `(this CLI is ${cliVersion}, a different major). Set ${VersionMatchEnv.OPT_OUT}=1 to keep this one.`);
107
+ }
108
+ /** Did the person name a version on the npx line (`npx @reticlehq/server@<version> …`)? */
109
+ function pinnedOnNpxLine(spec) {
110
+ if (spec === undefined || !spec.startsWith(`${SERVER_PACKAGE}@`))
111
+ return false;
112
+ return spec.slice(SERVER_PACKAGE.length + 1) !== LATEST_TAG;
113
+ }
114
+ function keepsThisBinary(argv) {
115
+ const [command, sub] = argv;
116
+ if (command !== undefined && KEEP_COMMANDS.has(command))
117
+ return true;
118
+ if (SETUP_COMMAND === command && SETUP_INSTALL === sub)
119
+ return true;
120
+ return argv.some((arg) => KEEP_FLAGS.has(arg));
121
+ }
122
+ /**
123
+ * The version to re-run at, or undefined to carry on in this process. Pure: the filesystem and the
124
+ * environment are handed in.
125
+ */
126
+ export function versionToMatch(input) {
127
+ const { env } = input;
128
+ if (env[VersionMatchEnv.REEXECUTED] !== undefined)
129
+ return undefined;
130
+ if (env[VersionMatchEnv.OPT_OUT] !== undefined)
131
+ return undefined;
132
+ if (pinnedOnNpxLine(env[VersionMatchEnv.NPX_PACKAGE]))
133
+ return undefined;
134
+ if (keepsThisBinary(input.argv))
135
+ return undefined;
136
+ const sdk = installedSdkVersion(input.projectDir, input.readFile);
137
+ if (sdk === undefined)
138
+ return undefined;
139
+ const sdkMajor = majorOf(sdk);
140
+ if (sdkMajor === undefined)
141
+ return undefined;
142
+ return sdkMajor === majorOf(input.cliVersion) ? undefined : sdk;
143
+ }
144
+ /** How to launch `npx --yes @reticlehq/server@<version> <args>` on this platform. */
145
+ export function npxInvocation(version, args, on) {
146
+ const argv = [NPX_YES, `${SERVER_PACKAGE}@${version}`, ...args];
147
+ // A `.cmd` cannot be spawned without a shell since the CVE-2024-27980 fix, and a shell needs every
148
+ // argument quoted — the same rule `npmSpawn` in the updater follows.
149
+ if (NodePlatform.WINDOWS !== on)
150
+ return { file: NPX, argv, shell: false };
151
+ return { file: [NPX_WINDOWS, ...argv.map(windowsShellArg)].join(' '), argv: [], shell: true };
152
+ }
153
+ /** A ChildProcess, seen through `ReexecChild`. */
154
+ export function asReexecChild(child) {
155
+ return {
156
+ onEnd: (listener) => {
157
+ // Node can emit `error` AND `exit` for one child; the listener exits the process, so once.
158
+ let ended = false;
159
+ const end = (code, error) => {
160
+ if (ended)
161
+ return;
162
+ ended = true;
163
+ listener(code, error);
164
+ };
165
+ child.once('error', (error) => end(undefined, error));
166
+ child.once('exit', (code) => end(code ?? undefined, undefined));
167
+ },
168
+ kill: (signal) => {
169
+ child.kill(signal);
170
+ },
171
+ };
172
+ }
173
+ /** The real spawner. */
174
+ export const nodeSpawner = (file, argv, options) => asReexecChild(spawn(file, argv, options));
175
+ const FORWARDED_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'];
176
+ const SPAWN_FAILED_EXIT = 1;
177
+ /**
178
+ * Hand this invocation to the matching version and exit with its code.
179
+ *
180
+ * `stdio: 'inherit'` gives the child this process's own file descriptors, so an MCP client's JSON-RPC
181
+ * stream reaches it without passing through a byte of JavaScript here — nothing to buffer, reframe or
182
+ * lose. For that to hold, this must run before anything writes to stdout.
183
+ */
184
+ export function reexecAtVersion(version, args, env, deps) {
185
+ const inv = npxInvocation(version, args, deps.platform ?? process.platform);
186
+ const child = deps.spawn(inv.file, inv.argv, {
187
+ stdio: 'inherit',
188
+ env: { ...env, [VersionMatchEnv.REEXECUTED]: MARKED },
189
+ shell: inv.shell,
190
+ });
191
+ // A client that stops the proxy signals THIS pid; the real server is the child.
192
+ const forward = (signal) => {
193
+ child.kill(signal);
194
+ };
195
+ for (const signal of FORWARDED_SIGNALS)
196
+ process.on(signal, forward);
197
+ child.onEnd((code, error) => {
198
+ for (const signal of FORWARDED_SIGNALS)
199
+ process.off(signal, forward);
200
+ if (error !== undefined) {
201
+ deps.warn?.(`Could not start ${SERVER_PACKAGE}@${version} to match this project's SDK (${error.message}). ` +
202
+ `Set ${VersionMatchEnv.OPT_OUT}=1 to use the installed CLI instead.`);
203
+ }
204
+ deps.exit(code ?? SPAWN_FAILED_EXIT);
205
+ });
206
+ }
@@ -17,7 +17,7 @@
17
17
  import { log } from '../../../log.js';
18
18
  import { RETICLE_VERIFY_DEFAULT_PORT } from '../../../index.js';
19
19
  import { verifyEndpointMismatch } from '../../../status-payload.js';
20
- import { probeDaemon } from '../../../surface/mcp/mcp-proxy.js';
20
+ import { probeDaemon, waitForDaemonBind } from '../../../surface/mcp/mcp-proxy.js';
21
21
  import { stateDirProblem } from '../../daemon/state-dir.js';
22
22
  import { readPid, isAlive, removePid, spawnDaemon, logPath, reticleStateHome, } from '../../daemon/daemon.js';
23
23
  import { readDaemonStartupCause } from '../../daemon/lifetime/startup-failure.js';
@@ -84,7 +84,7 @@ async function serveWithHonestExit(parsed) {
84
84
  // The child binds asynchronously and, when it cannot, exits 1 long after this process would have
85
85
  // reported success. Wait for it to ANSWER — `/status` responding is the only evidence a daemon
86
86
  // exists that does not come from the pid file the child may never have earned.
87
- const bound = await waitForPresence(parsed.port, PortPresence.DAEMON, SERVE_BIND_TIMEOUT_MS);
87
+ const bound = await waitForDaemonBind(parsed.port);
88
88
  if (!bound) {
89
89
  const settled = await probePresence(parsed.port, { tcpOpen: probeDaemon, status: fetchStatus });
90
90
  const startupCause = readDaemonStartupCause(logPath(parsed.port), startupStartedAt);
@@ -113,24 +113,10 @@ async function serveWithHonestExit(parsed) {
113
113
  }
114
114
  log('reticle_daemon_spawned', { port: parsed.port, ...(parsed.http ? { http: true } : {}) });
115
115
  }
116
- /** How long `serve` waits for the child to bind. Generous: a cold daemon start is seconds. */
117
- const SERVE_BIND_TIMEOUT_MS = 15_000;
118
- const PRESENCE_POLL_MS = 150;
119
116
  /** How long a daemon gets to shut down on its own before `stop` stops asking politely. */
120
117
  const GRACEFUL_STOP_MS = 5000;
121
118
  /** How long after SIGKILL before we accept the process is not ours to kill and say so. */
122
119
  const FORCED_STOP_MS = 2000;
123
- async function waitForPresence(port, want, timeoutMs) {
124
- const deadline = Date.now() + timeoutMs;
125
- for (;;) {
126
- const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
127
- if (presence === want)
128
- return true;
129
- if (Date.now() >= deadline)
130
- return false;
131
- await new Promise((resolve) => setTimeout(resolve, PRESENCE_POLL_MS));
132
- }
133
- }
134
120
  export function handleStop(port, quiet) {
135
121
  const pid = readPid(port);
136
122
  if (null === pid || !isAlive(pid)) {
@@ -23,6 +23,13 @@ export declare const RETICLE_CONFIG_BASENAME = ".reticle.json";
23
23
  * Returns undefined when nothing is found, which keeps every caller's existing default intact.
24
24
  */
25
25
  export declare function findProjectConfig(cwd: string): Record<string, unknown> | undefined;
26
+ /**
27
+ * The directory of the project this invocation serves: where its `.reticle.json` is, else the one
28
+ * wired workspace app below a monorepo root, else `cwd` itself. What the CLI reads the installed SDK
29
+ * from, so it answers for the app the port above belongs to rather than for the directory it was
30
+ * started in.
31
+ */
32
+ export declare function projectDirOf(cwd: string): string;
26
33
  /**
27
34
  * The dev-server port heuristics live in `@reticlehq/init` — `existing-config.ts` is what diagnoses
28
35
  * a `.reticle.json` whose `port` is the app's own dev-server port, and the scaffolder may not import
@@ -33,8 +40,20 @@ export { DEV_SERVER_PORTS, isLikelyDevServerPort, devServerPortWarning } from '@
33
40
  /**
34
41
  * Read the port stored in the project's .reticle.json (written by `reticle init`).
35
42
  * Returns undefined if the file is absent, unreadable, or has no valid numeric port.
43
+ *
44
+ * With no config at or above `cwd`, it looks DOWN, the same way `projectIdsAt` does. Running from a
45
+ * monorepo root whose app was wired in `apps/web` used to find nothing and fall back to the default
46
+ * port, so `status`, `mcp` and `verify` quietly talked to another project's daemon. One wired app
47
+ * below, or several that agree, give their port; several that disagree give none, and
48
+ * `workspacePortConflict` is what says so rather than letting the default win in silence.
36
49
  */
37
50
  export declare function readProjectPort(cwd: string): number | undefined;
51
+ /**
52
+ * The sentence for a monorepo root whose wired apps name different ports, or undefined when there is
53
+ * nothing to choose between. `readProjectPort` refuses to pick one; this is what tells the person
54
+ * which ports exist and how to name the one they meant.
55
+ */
56
+ export declare function workspacePortConflict(cwd: string): string | undefined;
38
57
  /**
39
58
  * Read the stable projectId stored in the project's .reticle.json (written by `reticle init`). The daemon
40
59
  * uses it as the default resolve scope so auto-selection stays within the active app. Returns
@@ -63,6 +82,12 @@ export declare function projectIdsAt(cwd: string): ProjectId[];
63
82
  * time and the hint the agent reads hours later never mentioned it.
64
83
  */
65
84
  export declare function readProjectFramework(cwd: string): string | undefined;
85
+ /**
86
+ * Whether the project this directory serves is a desktop shell (Electron, Tauri) — the same question
87
+ * `doctor` asks, answered from the same files. Read so the no-session guidance stops sending a
88
+ * desktop user to a browser URL.
89
+ */
90
+ export declare function readProjectIsDesktop(cwd: string): boolean;
66
91
  /**
67
92
  * Whether the durable causal journal is enabled. On by default (the journal IS the loop); off only via
68
93
  * explicit opt-out — `.reticle.json` `"journal": false`, or `RETICLE_JOURNAL` set to `0`/`false`. The env