@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
@@ -34,30 +34,57 @@ function daemonSitsInAProject(daemonRoot) {
34
34
  return false;
35
35
  }
36
36
  }
37
+ /**
38
+ * Every project this machine knows the directory of: discovered `.reticle.json` files first, then
39
+ * the user-level registry. Read on each call, since `init` can run in another terminal while the
40
+ * daemon is up.
41
+ */
42
+ function knownProjectCandidates() {
43
+ let registry = emptyProjectRegistry();
44
+ try {
45
+ const path = join(homedir(), ReticleDir.ROOT, PROJECT_REGISTRY_FILE);
46
+ registry = existsSync(path)
47
+ ? parseProjectRegistry(JSON.parse(readFileSync(path, 'utf8')))
48
+ : registry;
49
+ }
50
+ catch {
51
+ // A cache that cannot be read is an empty cache, never an error: the daemon still resolves
52
+ // through discovery, and falls back to its own root exactly as it did before this existed.
53
+ }
54
+ let discovery = { found: [], searched: [] };
55
+ try {
56
+ discovery = discoverProjectConfigs(process.cwd());
57
+ }
58
+ catch {
59
+ // Same reasoning: a diagnostic search that throws must not take a tool call with it.
60
+ }
61
+ return projectCandidatesFrom(discovery, registry);
62
+ }
63
+ /**
64
+ * The directory of the project a page announced, or undefined when this machine does not know it.
65
+ *
66
+ * A daemon is shared: started from `$HOME`, a monorepo root, or another project's MCP client, its
67
+ * cwd is often not the app that sent the HELLO. The version-skew remedy read `package.json` from
68
+ * cwd anyway, so it fell back to the generic sensor package and told a Next.js project to install
69
+ * `@reticlehq/browser` (#1135). This is the same lookup the artifact root uses, keyed by the page's
70
+ * own project id.
71
+ */
72
+ export function projectDirectoryFor(projectId) {
73
+ if (projectId === undefined)
74
+ return undefined;
75
+ const directories = new Set(knownProjectCandidates()
76
+ .filter((candidate) => candidate.projectId === projectId)
77
+ .map((candidate) => candidate.directory));
78
+ // Two checkouts declaring one id are refused, as the artifact root refuses them: naming one of
79
+ // them would read the wrong app's package.json and prescribe its packages to the other.
80
+ return 1 === directories.size ? [...directories][0] : undefined;
81
+ }
37
82
  export function artifactRootResolver(daemonRoot) {
38
83
  const daemonIsProject = daemonSitsInAProject(daemonRoot);
39
84
  return (projectId, origin) => {
40
- let registry = emptyProjectRegistry();
41
- try {
42
- const path = join(homedir(), ReticleDir.ROOT, PROJECT_REGISTRY_FILE);
43
- registry = existsSync(path)
44
- ? parseProjectRegistry(JSON.parse(readFileSync(path, 'utf8')))
45
- : registry;
46
- }
47
- catch {
48
- // A cache that cannot be read is an empty cache, never an error: the daemon still resolves
49
- // through discovery, and falls back to its own root exactly as it did before this existed.
50
- }
51
- let discovery = { found: [], searched: [] };
52
- try {
53
- discovery = discoverProjectConfigs(process.cwd());
54
- }
55
- catch {
56
- // Same reasoning: a diagnostic search that throws must not take a tool call with it.
57
- }
58
85
  const resolved = resolveArtifactRoot({
59
86
  projectId,
60
- candidates: projectCandidatesFrom(discovery, registry),
87
+ candidates: knownProjectCandidates(),
61
88
  daemonRoot,
62
89
  });
63
90
  if (resolved.reason === ArtifactRootReason.MATCHED_PROJECT)
@@ -33,6 +33,8 @@ export interface ReticleDirPaths {
33
33
  capsules: string;
34
34
  /**.../.reticle/flake.json (per-flow flake ledger) */
35
35
  flake: string;
36
+ /**.../.reticle/coverage.json (app-wide coverage ledger, all six levels) */
37
+ coverage: string;
36
38
  /**.../.reticle/assertion-tiers.json (last-passing assertion tiers; anti-reward-hacking baseline) */
37
39
  tiers: string;
38
40
  }
@@ -18,6 +18,7 @@ export function reticleDirPaths(root) {
18
18
  ambient: join(root, ReticleDir.AMBIENT_FILE),
19
19
  capsules: join(root, ReticleDir.CAPSULES_SUBDIR),
20
20
  flake: join(root, ReticleDir.FLAKE_FILE),
21
+ coverage: join(root, ReticleDir.COVERAGE_FILE),
21
22
  tiers: join(root, ReticleDir.TIERS_FILE),
22
23
  };
23
24
  }
@@ -38,7 +38,9 @@ function numericDelta(before, after) {
38
38
  * "what's broken vs before" from the cloud. Absent creds / unreachable → undefined (agent stays local).
39
39
  */
40
40
  async function cloudRegression(deps, sessionId) {
41
- const config = resolveCloudConfig(process.env);
41
+ // The same resolver every caller uses (stored key for a linked repo, else the exported one);
42
+ // the environment alone only for an embedder that wires no linked-credential port.
43
+ const config = await (deps.linkedCloud?.() ?? Promise.resolve(resolveCloudConfig(process.env)));
42
44
  if (null === config)
43
45
  return undefined;
44
46
  let projectId;
@@ -4,7 +4,7 @@
4
4
  * A daemon left running by one project answers the next project's app and rejects it on token, and
5
5
  * the SDK printed `bridge refused the connection: authentication failed`. That sends someone to
6
6
  * check a token which is perfectly correct — it is simply a DIFFERENT project's token. The two cases
7
- * need opposite fixes: "your token is wrong" versus "that daemon is not yours, stop it".
7
+ * need opposite fixes: "your token is wrong" versus "that daemon is not yours, use your own port".
8
8
  *
9
9
  * The discriminator is EVIDENCE, not derivation. The daemon and the SDK derive project ids by
10
10
  * different schemes (the SDK from its Vite root, the daemon from its own cwd), so comparing those
@@ -4,7 +4,7 @@
4
4
  * A daemon left running by one project answers the next project's app and rejects it on token, and
5
5
  * the SDK printed `bridge refused the connection: authentication failed`. That sends someone to
6
6
  * check a token which is perfectly correct — it is simply a DIFFERENT project's token. The two cases
7
- * need opposite fixes: "your token is wrong" versus "that daemon is not yours, stop it".
7
+ * need opposite fixes: "your token is wrong" versus "that daemon is not yours, use your own port".
8
8
  *
9
9
  * The discriminator is EVIDENCE, not derivation. The daemon and the SDK derive project ids by
10
10
  * different schemes (the SDK from its Vite root, the daemon from its own cwd), so comparing those
@@ -62,7 +62,9 @@ export function authFailureReason(servedProjects, helloProject, helloToken) {
62
62
  if (helloProject !== undefined &&
63
63
  1 === servedProjects.size &&
64
64
  !servedProjects.has(helloProject)) {
65
- const reason = `${DIFFERENT_PROJECT_PREFIX} — run \`reticle stop\` and retry`;
65
+ // Not "stop it": that daemon is serving somebody else's app. This project needs its own port,
66
+ // which the CLI and the build plugins both read from `.reticle.json`.
67
+ const reason = `${DIFFERENT_PROJECT_PREFIX} — set a free "port" in .reticle.json, then restart the dev server`;
66
68
  return Buffer.byteLength(reason, 'utf8') <= MAX_REASON_BYTES ? reason : PLAIN;
67
69
  }
68
70
  if (helloToken === undefined || 0 === helloToken.length)
@@ -38,6 +38,14 @@ export declare const WS_CLOSE_REASON: {
38
38
  * refusal on this path: nothing about the app is wrong and retrying is the correct response.
39
39
  */
40
40
  readonly HANDSHAKE_POOL_FULL: string;
41
+ /**
42
+ * A page's HELLO failed the wire schema for a reason other than its protocol number — the SDK and
43
+ * this daemon are different releases whose contracts no longer agree. The socket is still closed
44
+ * with the short `invalid message` the SDK already prints; this is the sentence the no-session
45
+ * diagnosis records, because "never dialled the bridge" was what `init` said about a page this
46
+ * daemon had just turned away. Same `cause — fix` shape as the protocol-skew reasons.
47
+ */
48
+ readonly INVALID_HELLO: string;
41
49
  };
42
50
  interface BridgeOptions {
43
51
  port: number;
@@ -58,10 +66,11 @@ interface BridgeOptions {
58
66
  */
59
67
  server?: http.Server;
60
68
  /**
61
- * The SDK-upgrade sentence to attach when a HELLO is skewed. Injected so the daemon can name
62
- * this project's packages and package manager; tests that omit it get the no-project fallback.
69
+ * The SDK-upgrade sentence to attach when a HELLO is skewed, for the project that HELLO named.
70
+ * Injected so the daemon can name that project's packages and package manager; tests that omit
71
+ * it get the no-project fallback.
63
72
  */
64
- sdkFix?: () => string;
73
+ sdkFix?: (projectId?: string) => string;
65
74
  }
66
75
  /**
67
76
  * The browser-facing half of the relay: a localhost WebSocket server. Each connection
@@ -69,6 +69,16 @@ export const WS_CLOSE_REASON = {
69
69
  'this dial never got to identify itself. Nothing is wrong with the app; the pool drains on its ' +
70
70
  'own and reloading the page reconnects. If it persists, something else on this machine is ' +
71
71
  'opening sockets to this port.',
72
+ /**
73
+ * A page's HELLO failed the wire schema for a reason other than its protocol number — the SDK and
74
+ * this daemon are different releases whose contracts no longer agree. The socket is still closed
75
+ * with the short `invalid message` the SDK already prints; this is the sentence the no-session
76
+ * diagnosis records, because "never dialled the bridge" was what `init` said about a page this
77
+ * daemon had just turned away. Same `cause — fix` shape as the protocol-skew reasons.
78
+ */
79
+ INVALID_HELLO: "invalid message: the page's hello does not match this daemon's wire contract — put " +
80
+ '@reticlehq/browser, the build plugin and @reticlehq/server on one release, run `reticle stop`, ' +
81
+ 'then restart the dev server',
72
82
  };
73
83
  /**
74
84
  * A dial turned away at the ORIGIN gate, in words the reader can act on.
@@ -179,6 +189,25 @@ function helloProtocolMismatch(text) {
179
189
  }
180
190
  return null;
181
191
  }
192
+ /**
193
+ * What a HELLO says about the page it came from, read loosely enough to survive the schema failing.
194
+ * Null when the message is not a hello at all — only an SDK sends one, so only then is a refusal
195
+ * evidence about an app.
196
+ */
197
+ function peekHelloPage(text) {
198
+ try {
199
+ const json = JSON.parse(text);
200
+ if (json.kind !== MessageKind.HELLO)
201
+ return null;
202
+ return {
203
+ ...('string' === typeof json.url ? { url: json.url } : {}),
204
+ ...('string' === typeof json.projectId ? { projectId: json.projectId } : {}),
205
+ };
206
+ }
207
+ catch {
208
+ return null;
209
+ }
210
+ }
182
211
  /** Normalize ws RawData (string | Buffer | Buffer[] | ArrayBuffer) into a UTF-8 string. */
183
212
  function rawToString(raw) {
184
213
  if ('string' === typeof raw)
@@ -436,10 +465,19 @@ export class Bridge {
436
465
  // Told to the AGENT too, not only to the SDK that is about to stop retrying. Without this
437
466
  // the next tool call answers "no browser session connected", which reads as "no app is
438
467
  // running" — so an SDK too old to connect is invisible.
439
- this.sessions.noteClosure(reason, this.#clock());
468
+ this.sessions.noteClosure(reason, this.#clock(), peekHelloPage(text) ?? {});
440
469
  socket.close(WS_CLOSE.PROTOCOL_MISMATCH[0], reason);
441
470
  return;
442
471
  }
472
+ // Recorded only for a HELLO on a socket that has none yet: that is a page being turned
473
+ // away, and it used to leave no trace, so `init` reported "never dialled the bridge" about
474
+ // a page this daemon had just refused. A malformed frame on a live session is a different
475
+ // failure and says nothing about whether the app can connect.
476
+ const refused = undefined === session ? peekHelloPage(text) : null;
477
+ if (null !== refused) {
478
+ log('hello_invalid', { ...refused });
479
+ this.sessions.noteClosure(WS_CLOSE_REASON.INVALID_HELLO, this.#clock(), refused);
480
+ }
443
481
  socket.close(...WS_CLOSE.INVALID_MESSAGE);
444
482
  return;
445
483
  }
@@ -498,7 +536,10 @@ export class Bridge {
498
536
  presented: parsed.token !== undefined && 0 < parsed.token.length,
499
537
  tokenSource: this.#tokenSource,
500
538
  });
501
- this.sessions.noteClosure(reason, this.#clock());
539
+ this.sessions.noteClosure(reason, this.#clock(), {
540
+ url: parsed.url,
541
+ ...(parsed.projectId === undefined ? {} : { projectId: parsed.projectId }),
542
+ });
502
543
  socket.close(WS_CLOSE.AUTH_FAILED[0], reason);
503
544
  return;
504
545
  }
@@ -567,6 +608,9 @@ export class Bridge {
567
608
  // The daemon is the single judge of skew, and HELLO is where the page announces itself.
568
609
  // Reported on the session (reticle_sessions) AND queued for the next tool result, because an
569
610
  // agent driving a flow never calls reticle_sessions and would never learn.
611
+ // Read only if the page IS skewed: the fix reads the project's package.json and lockfile,
612
+ // and a compatible page (nearly every HELLO) should not pay for that on connect.
613
+ const fixForThisProject = () => this.#sdkFix(parsed.projectId);
570
614
  const skew = describeSkew({
571
615
  what: 'the page',
572
616
  version: parsed.sdkVersion,
@@ -577,7 +621,9 @@ export class Bridge {
577
621
  ...(parsed.contractParts === undefined
578
622
  ? {}
579
623
  : { contractParts: parsed.contractParts }),
580
- fix: this.#sdkFix(),
624
+ get fix() {
625
+ return fixForThisProject();
626
+ },
581
627
  }, {
582
628
  version: SERVER_VERSION,
583
629
  contract: CONTRACT_FINGERPRINT,
@@ -0,0 +1,25 @@
1
+ import type { Page } from 'playwright';
2
+ /**
3
+ * V8 JS coverage on a page Reticle drives — the raw half of "what of the app ever ran".
4
+ *
5
+ * Started once per page with `resetOnNavigation: false`, so a reload keeps counting instead of
6
+ * starting over. A take stops and immediately restarts: the counts it returns are since the last
7
+ * take, and the reader folds them as a union (see features/exhaust/code-coverage.ts).
8
+ *
9
+ * Chromium only. Anything else — or a page that closed — answers false/undefined rather than
10
+ * throwing: coverage is an addition to a drive, never a reason for one to fail.
11
+ */
12
+ /** One script's coverage — the shape `stopJSCoverage` returns, narrowed to what is read. */
13
+ export interface ScriptCoverage {
14
+ url: string;
15
+ functions: {
16
+ functionName: string;
17
+ ranges: {
18
+ startOffset: number;
19
+ endOffset: number;
20
+ count: number;
21
+ }[];
22
+ }[];
23
+ }
24
+ export declare function startJsCoverage(page: Page, covering: WeakSet<Page>): Promise<boolean>;
25
+ export declare function takeJsCoverage(page: Page, covering: WeakSet<Page>): Promise<ScriptCoverage[] | undefined>;
@@ -0,0 +1,30 @@
1
+ export async function startJsCoverage(page, covering) {
2
+ if (covering.has(page))
3
+ return true;
4
+ try {
5
+ await page.coverage.startJSCoverage({ resetOnNavigation: false });
6
+ covering.add(page);
7
+ return true;
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }
13
+ export async function takeJsCoverage(page, covering) {
14
+ // Nothing collects until something asks: collection slows every script on the page. The first
15
+ // take starts it and has nothing to report yet.
16
+ if (!covering.has(page)) {
17
+ await startJsCoverage(page, covering);
18
+ return undefined;
19
+ }
20
+ try {
21
+ const entries = await page.coverage.stopJSCoverage();
22
+ covering.delete(page);
23
+ await startJsCoverage(page, covering);
24
+ return entries;
25
+ }
26
+ catch {
27
+ covering.delete(page);
28
+ return undefined;
29
+ }
30
+ }
@@ -9,6 +9,7 @@
9
9
  * Node-only. Playwright is loaded via DYNAMIC `import('playwright')` so non-CDP users never
10
10
  * pay for it; the type-only import is elided by `tsc`, so the build stays green without it.
11
11
  */
12
+ import { type ScriptCoverage } from './js-coverage.js';
12
13
  import type { Browser, Page } from 'playwright';
13
14
  import { ActionType, DriveErrorCode } from '@reticlehq/core';
14
15
  import { type MockRule } from './network-mock.js';
@@ -77,6 +78,14 @@ export interface RealInputProvider {
77
78
  * Optional: a provider with no owned browser simply omits it.
78
79
  */
79
80
  setMocks?(sessionUrl: string, rules: MockRule[]): Promise<boolean>;
81
+ /**
82
+ * What of the app's own code ran since the last take (V8 coverage, Chromium only), or undefined
83
+ * when no driven page matches or collection had not started. The first call starts collecting,
84
+ * because collection slows every script on the page and a slower page can let a console error land
85
+ * after a clean-console check passed. Optional: a provider with no owned browser cannot see the
86
+ * engine's counters.
87
+ */
88
+ takeCodeCoverage?(sessionUrl: string): Promise<ScriptCoverage[] | undefined>;
80
89
  /**
81
90
  * Pin the correlated page's viewport to fixed pixel dimensions so a screenshot baseline is
82
91
  * reproducible across machines (the missing piece of CI-stable visual regression, alongside masks
@@ -144,6 +153,7 @@ interface CdpProviderOptions {
144
153
  export declare class CdpRealInputProvider implements RealInputProvider {
145
154
  #private;
146
155
  constructor(options: CdpProviderOptions);
156
+ takeCodeCoverage(sessionUrl: string): Promise<ScriptCoverage[] | undefined>;
147
157
  isAvailableFor(sessionUrl: string): Promise<boolean>;
148
158
  perform(sessionUrl: string, action: ActionType, box: ElementBox, args: RealInputArgs): Promise<RealInputResult>;
149
159
  /** PNG of the correlated page, or undefined if none matches. */
@@ -236,6 +246,7 @@ export declare class LaunchedRealInputProvider implements OwnedRealInputProvider
236
246
  width: number;
237
247
  height: number;
238
248
  }): Promise<boolean>;
249
+ takeCodeCoverage(_sessionUrl: string): Promise<ScriptCoverage[] | undefined>;
239
250
  /** Apply network-mock rules to the owned page; false before navigate / after dispose. */
240
251
  setMocks(_sessionUrl: string, rules: MockRule[]): Promise<boolean>;
241
252
  /** Close the owned browser once. Idempotent and safe before navigate. */
@@ -1,6 +1,18 @@
1
+ /**
2
+ * Optional CDP/Playwright real-input mode.
3
+ *
4
+ * Synthetic `dispatchEvent` cannot drive native hover/pointer state (an onMouseEnter never
5
+ * fires, hit-testing never runs). When a CDP endpoint is configured, this module connects a
6
+ * Playwright `Browser` over CDP and drives REAL pointer/keyboard input against the element box
7
+ * the SDK resolves (viewport CSS px from getBoundingClientRect).
8
+ *
9
+ * Node-only. Playwright is loaded via DYNAMIC `import('playwright')` so non-CDP users never
10
+ * pay for it; the type-only import is elided by `tsc`, so the build stays green without it.
11
+ */
12
+ import { takeJsCoverage } from './js-coverage.js';
1
13
  import { stampedDriveUrl } from './drive-url-stamp.js';
2
- import { chromiumLaunchOptions } from '../../chromium-launch-options.js';
3
- import { gotoOptions } from '../pool/playwright-launcher.js';
14
+ import { launchChromium } from '../../launch-chromium.js';
15
+ import { chromiumLaunchHint, gotoOptions } from '../pool/playwright-launcher.js';
4
16
  import { BrowserLaunchKind } from '@reticlehq/core/telemetry';
5
17
  import { getSessionMetrics } from '../../telemetry/session-metrics.js';
6
18
  import { classifyConnectFailure } from '../../telemetry/connect-failure.js';
@@ -131,6 +143,8 @@ export class CdpRealInputProvider {
131
143
  /** Pages already listening. #pageFor resolves on EVERY call, so without this each action would add
132
144
  * another listener and every response would be emitted once per action taken so far. */
133
145
  #listening = new WeakSet();
146
+ /** Pages collecting V8 coverage — see js-coverage.ts. */
147
+ #covering = new WeakSet();
134
148
  #browser;
135
149
  constructor(options) {
136
150
  this.#cdpUrl = options.cdpUrl;
@@ -176,6 +190,10 @@ export class CdpRealInputProvider {
176
190
  this.#listen(page);
177
191
  return page;
178
192
  }
193
+ async takeCodeCoverage(sessionUrl) {
194
+ const page = await this.#pageFor(sessionUrl);
195
+ return page === undefined ? undefined : takeJsCoverage(page, this.#covering);
196
+ }
179
197
  async isAvailableFor(sessionUrl) {
180
198
  try {
181
199
  return (await this.#pageFor(sessionUrl)) !== undefined;
@@ -273,13 +291,14 @@ export const launchedChromium = async (headless) => {
273
291
  }
274
292
  const settle = getSessionMetrics().recordConnectAttempt(BrowserLaunchKind.LAUNCHED);
275
293
  try {
276
- const browser = await mod.chromium.launch(chromiumLaunchOptions(headless));
294
+ const browser = await launchChromium(mod.chromium, headless);
277
295
  settle();
278
296
  return browser;
279
297
  }
280
298
  catch (e) {
281
299
  settle(classifyConnectFailure(e));
282
- throw new DriveError(DriveErrorCode.LAUNCH_FAILED, e instanceof Error ? e.message : String(e));
300
+ const msg = e instanceof Error ? e.message : String(e);
301
+ throw new DriveError(DriveErrorCode.LAUNCH_FAILED, chromiumLaunchHint(msg) ?? msg);
283
302
  }
284
303
  };
285
304
  /**
@@ -296,6 +315,8 @@ export class LaunchedRealInputProvider {
296
315
  #onNetworkDetail;
297
316
  #browser;
298
317
  #page;
318
+ /** Pages collecting V8 coverage — see js-coverage.ts. */
319
+ #covering = new WeakSet();
299
320
  constructor(options) {
300
321
  this.#driveUrl = options.driveUrl;
301
322
  this.#headless = options.headless;
@@ -442,6 +463,10 @@ export class LaunchedRealInputProvider {
442
463
  await page.setViewportSize({ width: size.width, height: size.height });
443
464
  return true;
444
465
  }
466
+ takeCodeCoverage(_sessionUrl) {
467
+ const page = this.#livePage();
468
+ return page === undefined ? Promise.resolve(undefined) : takeJsCoverage(page, this.#covering);
469
+ }
445
470
  /** Apply network-mock rules to the owned page; false before navigate / after dispose. */
446
471
  async setMocks(_sessionUrl, rules) {
447
472
  const page = this.#livePage();
@@ -34,8 +34,8 @@ export const VIEWPORT_TOOLS = [
34
34
  recommendation: z.string().optional(),
35
35
  },
36
36
  handler: async (deps, args) => {
37
- const width = clampDim(args['width']);
38
- const height = clampDim(args['height']);
37
+ const width = clampDim('width', args['width']);
38
+ const height = clampDim('height', args['height']);
39
39
  const session = deps.sessions.resolve(asString(args['sessionId']));
40
40
  const provider = viewportProvider(deps);
41
41
  if (provider !== undefined) {
@@ -65,8 +65,18 @@ export const VIEWPORT_TOOLS = [
65
65
  },
66
66
  },
67
67
  ];
68
- /** Clamp a requested dimension into [MIN_DIM, MAX_DIM]; a missing/NaN value falls back to MIN_DIM. */
69
- function clampDim(value) {
70
- const n = 'number' === typeof value && Number.isFinite(value) ? Math.round(value) : MIN_DIM;
71
- return Math.max(MIN_DIM, Math.min(n, MAX_DIM));
68
+ /**
69
+ * Clamp a requested dimension into [MIN_DIM, MAX_DIM]; a missing value falls back to MIN_DIM.
70
+ *
71
+ * A value that is PRESENT but not a finite number is refused, not read as missing: a client that
72
+ * sent `"1440"` meant 1440, and answering with a 64px page it never asked for made every later
73
+ * snapshot, click and screenshot on that page read as broken (#1118).
74
+ */
75
+ function clampDim(key, value) {
76
+ if (value === undefined)
77
+ return MIN_DIM;
78
+ if ('number' !== typeof value || !Number.isFinite(value)) {
79
+ throw new Error(`${ReticleTool.VIEWPORT}: \`${key}\` must be a number of CSS px, got ${JSON.stringify(value)} — NOT applied`);
80
+ }
81
+ return Math.max(MIN_DIM, Math.min(Math.round(value), MAX_DIM));
72
82
  }
@@ -25,6 +25,12 @@ export declare function gotoOptions(timeoutMs: number | undefined): {
25
25
  export declare function playwrightLauncher(opts?: {
26
26
  headless?: boolean;
27
27
  }): Launcher;
28
+ /**
29
+ * Turn Playwright's raw launch error into the one command that fixes it, or undefined when it is
30
+ * not a missing-browser failure. Shared by the pooled and the drive launch, so `reticle verify` on
31
+ * a fresh machine gets the same advice as a lease.
32
+ */
33
+ export declare function chromiumLaunchHint(msg: string): string | undefined;
28
34
  /**
29
35
  * Resolve the pool's concurrency cap. An explicit RETICLE_MAX_CONTEXTS wins (clamped to
30
36
  * [1, hard ceiling]); otherwise scale with the machine but never above the CPU-derived ceiling.
@@ -6,7 +6,7 @@
6
6
  * Headless by default — the pool exists for fault-tolerant headless multi-agent testing.
7
7
  */
8
8
  import { BrowserLaunchKind } from '@reticlehq/core/telemetry';
9
- import { chromiumLaunchOptions } from '../../chromium-launch-options.js';
9
+ import { launchChromium } from '../../launch-chromium.js';
10
10
  import { getSessionMetrics } from '../../telemetry/session-metrics.js';
11
11
  import { classifyConnectFailure } from '../../telemetry/connect-failure.js';
12
12
  import { chromiumInstallCommand, chromiumInstallDepsCommand, bundledPlaywrightVersion, } from '../../command/cli/doctor/browser/chromium-hint.js';
@@ -105,27 +105,34 @@ export function playwrightLauncher(opts = {}) {
105
105
  // number that actually costs memory and the one that explains a slow machine.
106
106
  const settle = getSessionMetrics().recordConnectAttempt(BrowserLaunchKind.POOLED);
107
107
  try {
108
- const browser = wrapBrowser(await chromium.launch(chromiumLaunchOptions(headless)));
108
+ const browser = wrapBrowser(await launchChromium(chromium, headless));
109
109
  settle();
110
110
  return browser;
111
111
  }
112
112
  catch (err) {
113
113
  settle(classifyConnectFailure(err));
114
- // Turn Playwright's raw "Executable doesn't exist" into the one command that fixes it.
115
114
  const msg = err instanceof Error ? err.message : String(err);
116
- // Checked FIRST: Playwright's host-requirement validation error also contains
117
- // "browserType.launch", so the generic check below would otherwise misdiagnose it as a
118
- // missing binary and send the user through a fix that changes nothing.
119
- if (/host system is missing dependencies/i.test(msg)) {
120
- throw new Error(CHROMIUM_MISSING_DEPS_HINT);
121
- }
122
- if (/executable doesn.?t exist|playwright install|browsertype\.launch/i.test(msg)) {
123
- throw new Error(CHROMIUM_MISSING_HINT);
124
- }
125
- throw err;
115
+ const hint = chromiumLaunchHint(msg);
116
+ throw hint !== undefined ? new Error(hint) : err;
126
117
  }
127
118
  };
128
119
  }
120
+ /**
121
+ * Turn Playwright's raw launch error into the one command that fixes it, or undefined when it is
122
+ * not a missing-browser failure. Shared by the pooled and the drive launch, so `reticle verify` on
123
+ * a fresh machine gets the same advice as a lease.
124
+ */
125
+ export function chromiumLaunchHint(msg) {
126
+ // Checked FIRST: Playwright's host-requirement validation error also contains
127
+ // "browserType.launch", so the generic check below would otherwise misdiagnose it as a
128
+ // missing binary and send the user through a fix that changes nothing.
129
+ if (/host system is missing dependencies/i.test(msg))
130
+ return CHROMIUM_MISSING_DEPS_HINT;
131
+ if (/executable doesn.?t exist|playwright install|browsertype\.launch/i.test(msg)) {
132
+ return CHROMIUM_MISSING_HINT;
133
+ }
134
+ return undefined;
135
+ }
129
136
  const MAX_CONTEXTS_CEILING = 8;
130
137
  const MAX_CONTEXTS_FLOOR = 1;
131
138
  /**
@@ -12,8 +12,8 @@
12
12
  export interface DevCommand {
13
13
  /** The literal shell command, e.g. `pnpm run dev`. */
14
14
  command: string;
15
- /** The package.json script it runs. */
16
- script: string;
15
+ /** The package.json script it runs. Absent when the command runs a CLI directly (`npx tauri dev`). */
16
+ script?: string;
17
17
  /** The port the script PINS, when it pins one. Absent means the tool picks — never a guess. */
18
18
  port?: number;
19
19
  }
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { readFileSync } from 'node:fs';
12
12
  import { join } from 'node:path';
13
+ import { desktopLaunch } from '@reticlehq/init';
13
14
  /**
14
15
  * Script names checked, in order.
15
16
  *
@@ -47,23 +48,26 @@ function pinnedPort(script) {
47
48
  const port = Number.parseInt(digits, 10);
48
49
  return Number.isSafeInteger(port) ? port : undefined;
49
50
  }
50
- /** The scripts block of a package.json, or an empty one for anything unreadable/unparseable. */
51
- function readScripts(raw) {
51
+ /** The parsed package.json, or undefined for anything unreadable/unparseable. */
52
+ function readManifest(raw) {
52
53
  if (raw === undefined)
53
- return {};
54
+ return undefined;
54
55
  try {
55
- const parsed = JSON.parse(raw);
56
- if (null === parsed || 'object' !== typeof parsed)
57
- return {};
58
- const scripts = parsed.scripts;
59
- if (null === scripts || 'object' !== typeof scripts)
60
- return {};
61
- return scripts;
56
+ return JSON.parse(raw);
62
57
  }
63
58
  catch {
64
- return {};
59
+ return undefined;
65
60
  }
66
61
  }
62
+ /** The scripts block of a parsed package.json, or an empty one. */
63
+ function scriptsOf(manifest) {
64
+ if (null === manifest || 'object' !== typeof manifest)
65
+ return {};
66
+ const scripts = manifest.scripts;
67
+ if (null === scripts || 'object' !== typeof scripts)
68
+ return {};
69
+ return scripts;
70
+ }
67
71
  /**
68
72
  * The dev command for the project in `directory`, or undefined when there is not one to be sure of.
69
73
  *
@@ -71,9 +75,23 @@ function readScripts(raw) {
71
75
  * default disk reader.
72
76
  */
73
77
  export function detectDevCommand(directory, read = readTextFile) {
74
- const scripts = readScripts(read(join(directory, PACKAGE_JSON)));
78
+ const manifest = readManifest(read(join(directory, PACKAGE_JSON)));
79
+ const scripts = scriptsOf(manifest);
75
80
  const manager = LOCKFILES.find(({ file }) => read(join(directory, file)) !== undefined)?.manager ??
76
81
  DEFAULT_PACKAGE_MANAGER;
82
+ // A desktop shell's `dev` is its renderer's dev server, which no window ever opens onto. init's
83
+ // rule decides which launcher it is, so the command the daemon hands an agent and the one init
84
+ // spawns are the same.
85
+ const desktop = desktopLaunch(manifest);
86
+ if (desktop !== undefined) {
87
+ if ('command' in desktop)
88
+ return { command: desktop.command };
89
+ const command = `${manager} run ${desktop.script}`;
90
+ return {
91
+ command: 0 < desktop.args.length ? `${command} ${desktop.args}` : command,
92
+ script: desktop.script,
93
+ };
94
+ }
77
95
  for (const script of DEV_SCRIPTS) {
78
96
  const body = scripts[script];
79
97
  if ('string' !== typeof body || 0 === body.trim().length)