@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
@@ -14,7 +14,7 @@
14
14
  * and a real browser to find out what happens when neither works.
15
15
  */
16
16
  import { judgeWait, portBusyMessage, QUIET_MEANS_HUNG_MS, urlToWatch, WaitVerdict, } from './bringup/dev-server-wait.js';
17
- import { waitProgressLine } from './terminal/wait-progress.js';
17
+ import { connectProgressLine, waitProgressLine } from './terminal/wait-progress.js';
18
18
  /**
19
19
  * Windows gets longer to say something before silence counts as a wedge.
20
20
  *
@@ -28,7 +28,7 @@ import { waitProgressLine } from './terminal/wait-progress.js';
28
28
  */
29
29
  const WINDOWS_QUIET_MEANS_HUNG_MS = 3 * 60_000;
30
30
  const WINDOWS_QUIET_MEANS_HUNG_MS_APPLIES = 'win32' === process.platform;
31
- import { pickSession } from './session-pick.js';
31
+ import { liveSessionOfProject, pickSession } from './session-pick.js';
32
32
  import { readPage, describePage, findingBeforeOpen, PageFinding, } from './probe/page-probe.js';
33
33
  import { remainingSteps } from './remaining-steps.js';
34
34
  import { AppShape, isDesktop, policyFor } from './desktop-shape.js';
@@ -40,11 +40,27 @@ import { AppShape, isDesktop, policyFor } from './desktop-shape.js';
40
40
  const SDK_READY_WINDOW_MS = 15_000;
41
41
  /** The wait left when no browser was opened: only a tab that is ALREADY loaded can still appear. */
42
42
  const NO_BROWSER_GRACE_MS = 3_000;
43
+ /**
44
+ * How long a system browser that SAID it opened gets to produce a session before init proves the
45
+ * connect in a Reticle-owned browser instead. A launcher's exit is not a window: macOS `open` against
46
+ * a default browser that does not answer exits 1 only after the launch check has already counted it
47
+ * as opened, and init then waited out its whole connect budget over a correct install.
48
+ */
49
+ const SYSTEM_BROWSER_GRACE_MS = 15_000;
43
50
  export const SetupPhase = {
44
51
  DEV_SERVER: 'dev-server',
45
52
  CONNECT: 'connect',
46
53
  DONE: 'done',
47
54
  };
55
+ /** Hand a lease back. Best-effort: a lease that already expired is already gone. */
56
+ async function releaseLease(lease) {
57
+ try {
58
+ await lease?.release();
59
+ }
60
+ catch {
61
+ /* nothing to undo */
62
+ }
63
+ }
48
64
  const asProgress = (input, o) => ({
49
65
  initDone: true,
50
66
  devServerUp: undefined !== o.url,
@@ -89,6 +105,10 @@ async function daemonWhy(fx) {
89
105
  export async function runSetupPhases(input, fx) {
90
106
  const notes = [];
91
107
  const note = (line) => {
108
+ // The page is probed before the browser opens and again after the wait, and an unchanged page
109
+ // gave the reader the same paragraph twice in a row.
110
+ if (notes.at(-1) === line)
111
+ return;
92
112
  notes.push(line);
93
113
  fx.note(line);
94
114
  };
@@ -139,6 +159,7 @@ export async function runSetupPhases(input, fx) {
139
159
  serving,
140
160
  quietForMs: fx.devServerQuietForMs(),
141
161
  elapsedMs: fx.now() - startedAt,
162
+ budgetMs: input.startupBudgetMs,
142
163
  quietMeansHungMs: WINDOWS_QUIET_MEANS_HUNG_MS_APPLIES
143
164
  ? WINDOWS_QUIET_MEANS_HUNG_MS
144
165
  : QUIET_MEANS_HUNG_MS,
@@ -164,8 +185,12 @@ export async function runSetupPhases(input, fx) {
164
185
  // Says BOTH were checked. A server that prints nothing but IS listening is the CRA case and
165
186
  // must not be failed, so a reader has to be able to tell "we looked at the log" from "we
166
187
  // looked at the log AND the ports".
167
- note('The dev server neither printed a URL nor bound a port, so setup has nothing to open. ' +
168
- 'Check its log, or pass --url with the address it serves.');
188
+ note((input.startupBudgetMs !== undefined && fx.now() - startedAt >= input.startupBudgetMs
189
+ ? `The dev server did not become ready within ${String(input.startupBudgetMs)}ms. `
190
+ : undefined === watching
191
+ ? 'The dev server neither printed a URL nor bound a port, so setup has nothing to open. '
192
+ : `The dev server stopped making progress before ${watching} became ready. `) +
193
+ 'Check its log, increase --timeout for a slow build, or pass --url with the address it serves.');
169
194
  return stop(input, SetupPhase.DEV_SERVER, {}, notes);
170
195
  }
171
196
  await fx.sleep(input.pollMs);
@@ -181,9 +206,13 @@ export async function runSetupPhases(input, fx) {
181
206
  const requiredRuntime = isDesktop(input.shape) ? input.shape : undefined;
182
207
  // A tab already on this URL is the answer. Opening another is how three sessions appear.
183
208
  // `before` is empty so pickSession will take that tab rather than waiting for a new one.
209
+ //
210
+ // Not only when attached: a server init started itself (Next, Angular — nothing announces) has a
211
+ // url too, and a live tab of this project already on it is just as much the answer. Without this
212
+ // every re-run opened one more tab.
184
213
  const alreadyConnected = attachedToExisting
185
214
  ? pickSession(listed, url, new Set(), requiredRuntime)
186
- : null;
215
+ : liveSessionOfProject(listed, url, input.projectId, requiredRuntime);
187
216
  const before = new Set(listed.map((s) => s.sessionId));
188
217
  // Do not put a window in front of somebody until the page behind it can actually do something.
189
218
  //
@@ -198,7 +227,12 @@ export async function runSetupPhases(input, fx) {
198
227
  // proof that nothing can ever dial in.
199
228
  const budgetMs = input.connectBudgetMs ?? Math.max(input.phaseTimeoutMs, policy.connectBudgetMs);
200
229
  let openedBrowser = false;
201
- if (null === alreadyConnected && input.openBrowser && policy.openBrowser) {
230
+ let lease;
231
+ // `--no-open` used to skip this whole block, so nothing was opened and the run ended on "never
232
+ // dialled the bridge" over a correct install. With a lease to fall back on it is entered too, and
233
+ // only the system browser is skipped.
234
+ const canLease = undefined !== fx.openLease;
235
+ if (null === alreadyConnected && policy.openBrowser && (input.openBrowser || canLease)) {
202
236
  // SERVED is the precondition, not SDK_PRESENT: a url that answers nothing is the only state
203
237
  // where a window is certainly useless. Whether the SDK is in the served HTML is a DIFFERENT
204
238
  // question — Vite injects a marker, while Nuxt, React Router, Astro, SvelteKit and CRA deliver
@@ -209,7 +243,7 @@ export async function runSetupPhases(input, fx) {
209
243
  //
210
244
  // So presence decides only WHEN: open the moment it appears (the fast path for the frameworks
211
245
  // that inline it), otherwise once the short readiness window is out.
212
- const finding = await findingBeforeOpen(() => fx.probePage(url), { now: () => fx.now(), sleep: (ms) => fx.sleep(ms) }, Math.min(SDK_READY_WINDOW_MS, budgetMs), input.pollMs);
246
+ const finding = await findingBeforeOpen(() => fx.probePage(url), { now: () => fx.now(), sleep: (ms) => fx.sleep(ms) }, Math.min(SDK_READY_WINDOW_MS, budgetMs), input.pollMs, input.htmlCarriesSdk);
213
247
  if (PageFinding.NOT_SERVED === finding || PageFinding.TLS_REFUSED === finding) {
214
248
  note(describePage(finding, url));
215
249
  note('Not opening a browser: nothing answered that url, so the window could only show an error ' +
@@ -218,12 +252,23 @@ export async function runSetupPhases(input, fx) {
218
252
  else {
219
253
  // Said BEFORE the window appears, so somebody watching a page that stays inert has already
220
254
  // been told which of the two it is.
221
- if (PageFinding.SDK_MISSING === finding)
255
+ if (PageFinding.SDK_MISSING === finding && input.htmlCarriesSdk) {
222
256
  note(describePage(finding, url));
257
+ }
223
258
  // `= true` means ATTEMPTED, which is the most that is knowable — see openBrowser above for
224
259
  // why the launcher's own exit code must not be read as "no window appeared".
225
- await fx.openBrowser(url);
226
- openedBrowser = true;
260
+ const launcherFailed = input.openBrowser ? await fx.openBrowser(url) : undefined;
261
+ openedBrowser = input.openBrowser;
262
+ if (undefined !== fx.openLease && (!input.openBrowser || undefined !== launcherFailed)) {
263
+ const owned = await fx.openLease(url);
264
+ if ('failed' in owned) {
265
+ note(`Could not open ${url} in a Reticle-owned browser either: ${owned.failed}`);
266
+ }
267
+ else {
268
+ lease = owned;
269
+ openedBrowser = true;
270
+ }
271
+ }
227
272
  }
228
273
  }
229
274
  // Waiting the full budget for a session when nothing was opened to create one is dead time, and
@@ -237,6 +282,9 @@ export async function runSetupPhases(input, fx) {
237
282
  ? budgetMs
238
283
  : Math.min(NO_BROWSER_GRACE_MS, budgetMs));
239
284
  let session = alreadyConnected;
285
+ const waitStartedAt = fx.now();
286
+ let connectSpokeAtMs;
287
+ let leaseFallbackTried = false;
240
288
  while (null === session) {
241
289
  // On a desktop app, only the desktop window counts. AppShape and the runtime a page reports use
242
290
  // the same three names, so the shape IS the requirement — see session-pick.
@@ -245,6 +293,32 @@ export async function runSetupPhases(input, fx) {
245
293
  break;
246
294
  if (deadline <= fx.now())
247
295
  break;
296
+ if (!leaseFallbackTried &&
297
+ undefined === lease &&
298
+ undefined !== fx.openLease &&
299
+ openedBrowser &&
300
+ input.openBrowser &&
301
+ !isDesktop(input.shape) &&
302
+ fx.now() - waitStartedAt >= SYSTEM_BROWSER_GRACE_MS) {
303
+ leaseFallbackTried = true;
304
+ const owned = await fx.openLease(url);
305
+ if ('failed' in owned) {
306
+ note(`Could not open ${url} in a Reticle-owned browser either: ${owned.failed}`);
307
+ }
308
+ else {
309
+ lease = owned;
310
+ }
311
+ continue;
312
+ }
313
+ // Not in silence: a desktop run used to print one line and then nothing for ten minutes.
314
+ const waitedMs = fx.now() - waitStartedAt;
315
+ const progress = connectProgressLine(waitedMs, connectSpokeAtMs, deadline - waitStartedAt, policy.awaiting);
316
+ if (undefined !== progress) {
317
+ // To the person only: it is progress, not a finding, and in `notes` it would split the
318
+ // page description printed before and after this wait into two copies.
319
+ fx.note(progress);
320
+ connectSpokeAtMs = waitedMs;
321
+ }
248
322
  await fx.sleep(input.pollMs);
249
323
  }
250
324
  if (null === session) {
@@ -275,8 +349,13 @@ export async function runSetupPhases(input, fx) {
275
349
  fx.note(why.lead);
276
350
  notes.push(why.full ?? why.lead);
277
351
  }
278
- note(describePage(readPage(await fx.probePage(url)), url));
352
+ const finding = readPage(await fx.probePage(url));
353
+ // Silent on a bundle-connect shape: its HTML never carries the SDK, so the absence says nothing.
354
+ if (PageFinding.SDK_MISSING !== finding || input.htmlCarriesSdk) {
355
+ note(describePage(finding, url, undefined !== why));
356
+ }
279
357
  }
358
+ await releaseLease(lease);
280
359
  return stop(input, SetupPhase.CONNECT, { url }, notes);
281
360
  }
282
361
  // ── connected, which is where ONBOARDING ends ────────────────────────────────────────────────
@@ -292,16 +371,25 @@ export async function runSetupPhases(input, fx) {
292
371
  // Both routes, because `explore` needs a model: without ANTHROPIC_API_KEY it answers "No model
293
372
  // configured to drive the app". Naming only that one hands the reader a dead end on any machine
294
373
  // without a key, which is the same defect this release spent its time removing everywhere else.
295
- // Three short lines rather than one paragraph, and each says who it is for.
374
+ // A heading and a numbered list rather than one paragraph: a reader scans for what to do next.
296
375
  //
297
376
  // This was a single 524-character note, measured on a real first run: eighty-three words, no
298
377
  // break, naming `reticle_*` tools at a reader who had just typed `reticle init` in a terminal and
299
378
  // has no such tools to call. The facts were right and the shape made them unreadable, which on
300
379
  // the last line of onboarding is the same as not saying them.
301
- note(`Connected. ${url} is instrumented. Onboarding is done; nothing is verified yet.`);
302
- note('Agent: drive one flow and end it with `reticle_act_and_wait` or `reticle_assert`. Those two ' +
380
+ note(`✓ Connected. ${url} is instrumented. Onboarding is done; nothing is verified yet.`);
381
+ // Said, because "connected" otherwise reads as "your browser connected": the page that dialled
382
+ // was a headless one Reticle launched and has now closed, and nothing is open on this screen.
383
+ if (undefined !== lease && session.sessionId === lease.sessionId) {
384
+ note('The proof came from a Reticle-owned headless browser (now closed), not a window of yours — ' +
385
+ `open ${url} in your own browser to use the app.`);
386
+ }
387
+ await releaseLease(lease);
388
+ note('');
389
+ note('Next, prove one flow. Your agent does this:');
390
+ note(' 1. Drive one flow and end it with `reticle_act_and_wait` or `reticle_assert`. Those two ' +
303
391
  'produce a verdict; nothing else does.');
304
- note('Or hand over the whole drive: `reticle_verify { action: "explore", persona: "<who does ' +
392
+ note(' 2. Or hand over the whole drive: `reticle_verify { action: "explore", persona: "<who does ' +
305
393
  'what>" }` records what it drove, so later runs replay with no model in the loop. Needs ' +
306
394
  'ANTHROPIC_API_KEY.');
307
395
  return {
@@ -21,7 +21,20 @@ export interface CandidateSession {
21
21
  readonly hasCapabilities?: boolean;
22
22
  /** Which shell answered. Absent on an SDK too old to report one — see requiredRuntime below. */
23
23
  readonly runtime?: string;
24
+ /** The project the page reported. Absent on an SDK too old to report one. */
25
+ readonly projectId?: string;
24
26
  }
27
+ /**
28
+ * A tab of THIS project on `url` that is demonstrably alive right now, or null.
29
+ *
30
+ * Stricter than `pickSession`, because it decides whether to open a window at all: every init run
31
+ * opened another tab even when this project already had a live one on the url, and the pile-up then
32
+ * made every tool called without a sessionId fail with "multiple sessions connected". Reused only on
33
+ * positive evidence — the page names this project, is visible, and was heard from inside the
34
+ * daemon's staleness threshold — because reusing a dead tab is the worse mistake: nothing opens and
35
+ * nothing connects.
36
+ */
37
+ export declare function liveSessionOfProject(sessions: readonly CandidateSession[], url: string, projectId: string | undefined, requiredRuntime?: string): CandidateSession | null;
25
38
  /**
26
39
  * The session to drive, or null when nothing on this url qualifies.
27
40
  *
@@ -10,9 +10,29 @@
10
10
  * the oldest: a tab whose dev server died yesterday. The HUD then plays to a window nobody is
11
11
  * watching, and the verdict describes a page the user cannot see.
12
12
  */
13
+ import { SESSION_HEALTH } from '@reticlehq/core';
13
14
  /** Sorts a hidden tab after a visible one; among equals, the least stale first. */
14
15
  const isLive = (s) => true !== s.hidden && true !== s.throttled;
15
16
  const staleness = (s) => s.lastSeenMs ?? Number.POSITIVE_INFINITY;
17
+ /**
18
+ * A tab of THIS project on `url` that is demonstrably alive right now, or null.
19
+ *
20
+ * Stricter than `pickSession`, because it decides whether to open a window at all: every init run
21
+ * opened another tab even when this project already had a live one on the url, and the pile-up then
22
+ * made every tool called without a sessionId fail with "multiple sessions connected". Reused only on
23
+ * positive evidence — the page names this project, is visible, and was heard from inside the
24
+ * daemon's staleness threshold — because reusing a dead tab is the worse mistake: nothing opens and
25
+ * nothing connects.
26
+ */
27
+ export function liveSessionOfProject(sessions, url, projectId, requiredRuntime) {
28
+ if (undefined === projectId || 0 === projectId.length)
29
+ return null;
30
+ const mine = sessions.filter((s) => projectId === s.projectId &&
31
+ true !== s.hidden &&
32
+ undefined !== s.lastSeenMs &&
33
+ s.lastSeenMs <= SESSION_HEALTH.STALE_THRESHOLD_MS);
34
+ return pickSession(mine, url, new Set(), requiredRuntime);
35
+ }
16
36
  /**
17
37
  * The session to drive, or null when nothing on this url qualifies.
18
38
  *
@@ -27,22 +27,10 @@ interface SetupCommandInput extends Omit<SetupInput, 'shape'> {
27
27
  readonly env: Readonly<Record<string, string>>;
28
28
  /** Register the MCP server with the coding agents init does not itself reach. */
29
29
  readonly registerAgents: boolean;
30
+ /** The bridge's pairing token, for the MCP transport a connect-proof lease is opened over. */
31
+ readonly pairingToken?: string | undefined;
30
32
  }
31
- /**
32
- * Register with the agents `init` does not reach, and leave the skill where one loads skills from.
33
- *
34
- * init covers eight clients and only where it finds them installed, which leaves VS Code's USER
35
- * scope unwritten — it exists on machines today, and init only ever writes the project-scope file,
36
- * so a VS Code user has no tools outside the directory they ran init in.
37
- */
38
33
  export declare function registerOtherAgents(print: (line: string) => void): void;
39
34
  type SetupCommandResult = SetupOutcome;
40
- /**
41
- * Run the phases against the real world.
42
- *
43
- * The dev server is stopped on every ending except success, where it is handed to the user: an
44
- * instrumented app they can watch is the deliverable, and killing it would leave them with config
45
- * files and a dead tab.
46
- */
47
35
  export declare function runSetupCommand(input: SetupCommandInput, print: (line: string) => void): Promise<SetupCommandResult>;
48
36
  export {};
@@ -9,22 +9,25 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
9
9
  import { homedir } from 'node:os';
10
10
  import { join } from 'node:path';
11
11
  import { planAgentConfigs } from './agent-configs.js';
12
- import { AppShape, readShape } from './desktop-shape.js';
12
+ import { AppShape, describeShape, readShape } from './desktop-shape.js';
13
13
  import { stopOnInterrupt } from './terminal/interrupt.js';
14
14
  import { stopOnCrash, crashSentence } from './terminal/crash.js';
15
15
  import { applyAgentPlan, applyAgentSkills } from './agent-writer.js';
16
16
  import { ApprovalOutcome, grantAutoApproval } from './auto-approve.js';
17
17
  import { agentIo } from './agent-io.js';
18
- import { EnsureDaemon, ensureDaemon, nodeEnsureDaemonDeps } from './bringup/ensure-daemon.js';
18
+ import { EnsureDaemon, ensureDaemon, holdDaemon, nodeEnsureDaemonDeps, nodeRetireDeps, retireMovedDaemons, } from './bringup/ensure-daemon.js';
19
19
  import { openInBrowser } from '../cli/launch/cli-launch.js';
20
+ import { openLeaseFor } from './bringup/connect-lease.js';
20
21
  import { readProjectId } from '../cli/ports/resolve/cli-port.js';
21
- import { reticleStateHome } from '../daemon/daemon.js';
22
+ import { spawnSync } from 'node:child_process';
23
+ import { isAlive, reticleStateHome } from '../daemon/daemon.js';
22
24
  import { readDevServers } from '../daemon/dev-servers.js';
23
25
  import { urlOfExistingApp } from './probe/existing-app.js';
26
+ import { handedOverUrl, stopHandedOver } from './bringup/dev-server-files.js';
24
27
  import { listSessions, OwnedDevServer, probePage } from './node-effects.js';
25
28
  import { fetchStatus } from '../daemon/binding/daemon-status-probe.js';
26
- import { summarizeStatus } from '../cli/launch/cli-launch.js';
27
- import { runSetupPhases, } from './run-setup.js';
29
+ import { daemonSkew, summarizeStatus } from '../cli/launch/cli-launch.js';
30
+ import { runSetupPhases, SetupPhase, } from './run-setup.js';
28
31
  import { noticeKey, readSaid, rememberSaid, unsaidNotices } from './agent-notice-memory.js';
29
32
  /**
30
33
  * The dev server a crash should take with it, if one is running right now.
@@ -84,13 +87,15 @@ function hasElectronDependency(dir) {
84
87
  * scope unwritten — it exists on machines today, and init only ever writes the project-scope file,
85
88
  * so a VS Code user has no tools outside the directory they ran init in.
86
89
  */
90
+ /** `agent`, `agents`. */
91
+ const agentWord = (n) => (1 === n ? 'agent' : 'agents');
87
92
  export function registerOtherAgents(print) {
88
93
  const platform = process.platform;
89
94
  const home = homedir();
90
95
  const results = applyAgentPlan(planAgentConfigs({ home, platform, exists: agentIo.exists, readFile: agentIo.readFile }), agentIo);
91
96
  const wrote = results.filter((r) => 'created' === r.action || 'merged' === r.action);
92
97
  if (0 < wrote.length) {
93
- print(`registered the MCP server with ${wrote.length} more agent(s): ${wrote.map((r) => r.name).join(', ')}`);
98
+ print(` ✓ ${String(wrote.length)} more ${agentWord(wrote.length)}: ${wrote.map((r) => r.name).join(', ')}`);
94
99
  }
95
100
  // A format we will not rewrite is somebody's to edit, so it has to be said rather than skipped --
96
101
  // but said ONCE. Two commands call this function by design, so a `curl | sh` followed by
@@ -103,21 +108,21 @@ export function registerOtherAgents(print) {
103
108
  .map((r) => ({ name: r.name, file: r.file, why: r.why }));
104
109
  const fresh = unsaidNotices(readSaid(stateHome), manual);
105
110
  for (const notice of fresh) {
106
- print(`${notice.name}: ${notice.why} — add the reticle entry to ${notice.file} by hand.`);
111
+ print(` ⚠ ${notice.name}: ${notice.why} — add the reticle entry to ${notice.file} by hand.`);
107
112
  }
108
113
  rememberSaid(stateHome, fresh.map((n) => noticeKey(n)));
109
114
  const skills = applyAgentSkills(agentIo, { home, platform });
110
115
  if (0 < skills.length)
111
- print(`wrote the /reticle skill for ${skills.length} agent(s)`);
116
+ print(` ✓ the /reticle skill, for ${String(skills.length)} ${agentWord(skills.length)}`);
112
117
  // Registration alone still leaves an Accept dialog in front of every call, and a verification run
113
118
  // makes dozens of them: the loop is only autonomous once the tools are pre-approved.
114
119
  const approvals = grantAutoApproval(agentIo, { home, platform });
115
120
  const granted = approvals.filter((a) => ApprovalOutcome.GRANTED === a.outcome);
116
121
  if (0 < granted.length) {
117
- print(`pre-approved the reticle tools in ${granted.map((a) => a.name).join(', ')} — no Accept prompt per call`);
122
+ print(` ✓ tools pre-approved in ${granted.map((a) => a.name).join(', ')}, so no Accept prompt per call`);
118
123
  }
119
124
  for (const noted of approvals.filter((a) => undefined !== a.warn)) {
120
- print(`${noted.name}: ${String(noted.warn)}`);
125
+ print(` ⚠ ${noted.name}: ${String(noted.warn)}`);
121
126
  }
122
127
  }
123
128
  /**
@@ -127,13 +132,70 @@ export function registerOtherAgents(print) {
127
132
  * instrumented app they can watch is the deliverable, and killing it would leave them with config
128
133
  * files and a dead tab.
129
134
  */
135
+ /** How long a stopped dev server gets to let go of its port before the new one is started. */
136
+ const HANDED_OVER_EXIT_MS = 10_000;
137
+ const HANDED_OVER_EXIT_POLL_MS = 100;
138
+ /** Stop the server a previous init handed over for `appDir`, and wait (bounded) for it to exit. */
139
+ async function restartHandedOverServer(appDir) {
140
+ let stoppedPid;
141
+ const stopped = stopHandedOver(reticleStateHome(), appDir, {
142
+ alive: isAlive,
143
+ kill: (pid) => {
144
+ stoppedPid = pid;
145
+ try {
146
+ // Its own process group (it was spawned detached), so what it started goes with it.
147
+ if ('win32' === process.platform) {
148
+ spawnSync('taskkill', ['/PID', String(pid), '/T', '/F']);
149
+ }
150
+ else {
151
+ process.kill(-pid, 'SIGTERM');
152
+ }
153
+ }
154
+ catch {
155
+ /* already gone */
156
+ }
157
+ },
158
+ });
159
+ const deadline = Date.now() + HANDED_OVER_EXIT_MS;
160
+ while (undefined !== stoppedPid && isAlive(stoppedPid) && Date.now() < deadline) {
161
+ await new Promise((done) => setTimeout(done, HANDED_OVER_EXIT_POLL_MS));
162
+ }
163
+ return stopped;
164
+ }
130
165
  export async function runSetupCommand(input, print) {
131
166
  if (input.registerAgents)
132
167
  registerOtherAgents(print);
168
+ // A moved port first: this project's daemon on the OLD port would otherwise be the one the page
169
+ // finds and dials, while everything below waits on the new one.
170
+ const retired = await retireMovedDaemons(readProjectId(input.appDir), input.bridgePort, nodeRetireDeps());
171
+ for (const port of retired) {
172
+ print(`stopped this project's Reticle daemon on port ${String(port)}: the project now uses ` +
173
+ `${String(input.bridgePort)}, and the page would have kept dialling the old one.`);
174
+ }
175
+ // And the dev server a previous init left running, which may have read the old port once at
176
+ // startup (Next does) — so the attach below would keep a page dialling the daemon just stopped.
177
+ if (0 < retired.length && (await restartHandedOverServer(input.appDir))) {
178
+ print(`stopped the dev server a previous init started, so it restarts on port ${String(input.bridgePort)}.`);
179
+ }
133
180
  // Before the app is booted, because the app's whole job from here is to dial this port. Without
134
181
  // it the phases wait out their budget and then report the SDK as the thing that failed.
135
- const daemon = await ensureDaemon(input.bridgePort, nodeEnsureDaemonDeps());
136
- if (EnsureDaemon.UNAVAILABLE === daemon) {
182
+ const daemon = await ensureDaemon(input.bridgePort, nodeEnsureDaemonDeps((status) => undefined !== daemonSkew(status)));
183
+ if (undefined !== daemon.message)
184
+ print(daemon.message);
185
+ if (EnsureDaemon.SKEWED_IN_USE === daemon.state) {
186
+ // Stop here: every hello from the page would be refused, and waiting out the connect budget
187
+ // only to blame the page is the run this exists to prevent.
188
+ return {
189
+ ok: false,
190
+ reachedPhase: SetupPhase.DEV_SERVER,
191
+ flowSaved: false,
192
+ notes: undefined === daemon.message ? [] : [daemon.message],
193
+ fallback: [
194
+ `Once those sessions are done, free the port with \`npx @reticlehq/server kill --port ${String(input.bridgePort)}\` and re-run init.`,
195
+ ],
196
+ };
197
+ }
198
+ if (EnsureDaemon.UNAVAILABLE === daemon.state) {
137
199
  print(`could not start the Reticle daemon on port ${String(input.bridgePort)}, so nothing is listening for the app to connect to. Run \`npx @reticlehq/server serve --port ${String(input.bridgePort)}\` and try again.`);
138
200
  }
139
201
  // Which shell this is decides three things the phases would otherwise get wrong: opening a
@@ -145,7 +207,7 @@ export async function runSetupCommand(input, print) {
145
207
  hasElectronDep: hasElectronDependency(input.appDir),
146
208
  });
147
209
  if (AppShape.WEB !== shape)
148
- print(`detected a ${shape} app`);
210
+ print(`detected ${describeShape(shape)}`);
149
211
  const server = new OwnedDevServer();
150
212
  const effects = {
151
213
  startDevServer: (command, cwd) => {
@@ -153,10 +215,16 @@ export async function runSetupCommand(input, print) {
153
215
  server.start(command, cwd, input.env);
154
216
  return Promise.resolve();
155
217
  },
156
- existingAppUrl: () => Promise.resolve(urlOfExistingApp(readDevServers(reticleStateHome()), {
218
+ // The server the plugin announced (Vite), or failing that the one a previous init handed over
219
+ // and recorded — the only way to know a Next or Angular server on the port is this project's own.
220
+ existingAppUrl: async () => urlOfExistingApp(readDevServers(reticleStateHome()), {
157
221
  projectId: readProjectId(input.appDir),
158
222
  root: input.appDir,
159
- })),
223
+ }) ??
224
+ (await handedOverUrl(reticleStateHome(), input.appDir, {
225
+ alive: isAlive,
226
+ answers: async (url) => (await probePage(url)).served,
227
+ })),
160
228
  devServerOutput: () => server.output(),
161
229
  devServerExited: () => server.exited(),
162
230
  devServerQuietForMs: () => server.quietForMs(),
@@ -164,20 +232,16 @@ export async function runSetupCommand(input, print) {
164
232
  probePage,
165
233
  openBrowser: async (url) => {
166
234
  const failure = await openInBrowser(url);
167
- if (null !== failure) {
168
- print(`could not open a browser (${failure}). Nothing was opened, and nothing else here will ` +
169
- // Three wordings have been wrong in a row, each naming a way out that does not exist on
170
- // the machine being spoken to. It said `reticle_run({ tool: "reticle_lease", ... })`:
171
- // the default surface advertises neither name and ships no hatch to reach the second.
172
- // It then said `reticle open <url>`, which asks the OS to launch the default browser --
173
- // the very thing that just failed -- so it returned the reader to the same wall.
174
- // This one names no command: the missing piece is a browser, and the honest sentence
175
- // says which url needs one rather than inventing a Reticle that can conjure it.
176
- `open one: this asks the OS for your default browser, and it is a machine with none — ` +
177
- `CI, a container, an SSH session, WSL with no host browser. Point any browser that can ` +
178
- `reach it at ${url}; the session appears within a second of the page loading.`);
179
- }
235
+ if (null === failure)
236
+ return undefined;
237
+ // It used to end "nothing else here will open one", which stopped being true once a
238
+ // Reticle-owned browser could always launch: run-setup opens a lease on this failure, so the
239
+ // reader is told what happens next rather than sent to find a browser by hand.
240
+ print(`could not open a browser (${failure}), so the connect proof will come from a ` +
241
+ `Reticle-owned headless browser instead. To use the app yourself, point any browser at ${url}.`);
242
+ return failure;
180
243
  },
244
+ openLease: (url) => openLeaseFor(input.bridgePort, url, input.pairingToken),
181
245
  listSessions: () => listSessions(input.bridgePort),
182
246
  // `summarizeStatus` already narrows this payload for `reticle status`; reusing it here keeps one
183
247
  // reader of the wire shape rather than two that can disagree about which key carries the reason.
@@ -207,16 +271,26 @@ export async function runSetupCommand(input, print) {
207
271
  activeDevServerStop = () => {
208
272
  server.stop();
209
273
  };
274
+ // Hold the daemon for as long as this run waits on it. Its idle rule counted none of this as
275
+ // activity, and on a slow desktop build the daemon init had just started idled out mid-wait.
276
+ const releaseDaemon = holdDaemon(input.bridgePort);
210
277
  try {
211
- const outcome = await runSetupPhases({ ...input, shape }, effects);
278
+ const outcome = await runSetupPhases({ ...input, shape, projectId: readProjectId(input.appDir) }, effects);
212
279
  // The app stays up only when there is something worth watching.
213
- if (outcome.ok)
214
- server.handOver();
280
+ if (outcome.ok) {
281
+ server.handOver(outcome.url);
282
+ // Its output is on a file now, not this terminal, so say where: a compile error that used to
283
+ // scroll past here is the first thing somebody will look for.
284
+ const log = server.logPath();
285
+ if (undefined !== log)
286
+ print(`The dev server keeps running; its output goes to ${log}`);
287
+ }
215
288
  return {
216
289
  ...outcome,
217
290
  };
218
291
  }
219
292
  finally {
293
+ releaseDaemon();
220
294
  releaseSignals();
221
295
  activeDevServerStop = null;
222
296
  server.stop();
@@ -32,6 +32,8 @@ export interface SetupMcpResult {
32
32
  readonly detected: readonly string[];
33
33
  readonly registered: readonly string[];
34
34
  readonly alreadyThere: readonly string[];
35
+ /** Automatic registration was attempted but failed. */
36
+ readonly failed: readonly string[];
35
37
  /**
36
38
  * Clients whose config this will NOT rewrite, and which therefore still need a human.
37
39
  *
@@ -1,9 +1,10 @@
1
- import { McpClient, MCP_CLIENTS, claudeAddCommand, claudeAvailableProbe, claudeExistsProbe, detectMcpClients, mergeClientConfig, clientSpec, ClientMergeStatus, } from '@reticlehq/init';
1
+ import { McpClient, MCP_CLIENTS, claudeAddCommand, claudeAvailableProbe, claudeHasReticle, detectMcpClients, mergeClientConfig, clientSpec, ClientMergeStatus, } from '@reticlehq/init';
2
2
  import { OnboardingPhase, OnboardingStepStatus, } from '@reticlehq/core/telemetry';
3
3
  export function setupMcp(io) {
4
4
  const detected = [];
5
5
  const registered = [];
6
6
  const alreadyThere = [];
7
+ const failed = [];
7
8
  const manual = [];
8
9
  /*
9
10
  * Claude Code first, because nothing else can find it.
@@ -22,14 +23,16 @@ export function setupMcp(io) {
22
23
  const available = claudeAvailableProbe();
23
24
  if (io.runCli(available.command, available.args)) {
24
25
  detected.push(McpClient.CLAUDE_CODE);
25
- const already = claudeExistsProbe();
26
- if (io.runCli(already.command, already.args)) {
26
+ // Read from Claude's config: `claude mcp get` health-checks by launching the server.
27
+ if (claudeHasReticle(io, undefined)) {
27
28
  alreadyThere.push(McpClient.CLAUDE_CODE);
28
29
  }
29
30
  else {
30
31
  const cmd = claudeAddCommand();
31
32
  if (io.runCli(cmd.command, cmd.args))
32
33
  registered.push(McpClient.CLAUDE_CODE);
34
+ else
35
+ failed.push(McpClient.CLAUDE_CODE);
33
36
  }
34
37
  }
35
38
  for (const client of detectMcpClients(io)) {
@@ -47,8 +50,13 @@ export function setupMcp(io) {
47
50
  manual.push({ id: client.id, configPath: client.configPath, docs: spec.docs });
48
51
  continue;
49
52
  }
50
- io.writeFile(client.configPath, merged.content);
51
- registered.push(client.id);
53
+ try {
54
+ io.writeFile(client.configPath, merged.content);
55
+ registered.push(client.id);
56
+ }
57
+ catch {
58
+ failed.push(client.id);
59
+ }
52
60
  }
53
61
  /*
54
62
  * Both INSTALL steps, reported from the one place that knows the answer.
@@ -69,7 +77,8 @@ export function setupMcp(io) {
69
77
  * SKIPPED when there was nothing to register with — not failed. Nothing went wrong; there was
70
78
  * simply no agent here, and counting that as our failure would hide the ones that are.
71
79
  *
72
- * FAILED when every client we found needs a hand. That case used to fall through to COMPLETED,
80
+ * FAILED when no detected client is usable, whether registration failed or needs a hand.
81
+ * That case used to fall through to COMPLETED,
73
82
  * so the cohort with the tools registered NOWHERE was counted among the successful installs, and
74
83
  * the one number that says "did this machine get Reticle" read yes for the people it read no
75
84
  * for. Registering some and not others is still COMPLETED: the agent they are using may well be
@@ -79,11 +88,11 @@ export function setupMcp(io) {
79
88
  ? OnboardingStepStatus.SKIPPED
80
89
  : 0 === registered.length && alreadyThere.length > 0
81
90
  ? OnboardingStepStatus.SKIPPED
82
- : 0 === registered.length && 0 === alreadyThere.length && manual.length > 0
91
+ : 0 === registered.length && 0 === alreadyThere.length
83
92
  ? OnboardingStepStatus.FAILED
84
93
  : OnboardingStepStatus.COMPLETED,
85
94
  });
86
- return { detected, registered, alreadyThere, manual };
95
+ return { detected, registered, alreadyThere, failed, manual };
87
96
  }
88
97
  /** Everything this machine could be asked about, for the report when nothing was found. */
89
98
  export function knownClientLabels() {
@@ -26,3 +26,11 @@ export declare const WAIT_PROGRESS_EVERY_MS = 15000;
26
26
  * without a line per poll burying the output it exists to explain.
27
27
  */
28
28
  export declare function waitProgressLine(elapsedMs: number, watching: string | undefined, lastSpokeAtMs: number | undefined): string | undefined;
29
+ /**
30
+ * The same, for the wait on a session after the app is up.
31
+ *
32
+ * That wait was silent as well: a desktop init printed "Waiting for the app to launch and dial in."
33
+ * and then nothing for the whole of its budget. Naming what is awaited and how long remains is what
34
+ * lets somebody tell a slow start from a wedged one — and when to stop waiting themselves.
35
+ */
36
+ export declare function connectProgressLine(elapsedMs: number, lastSpokeAtMs: number | undefined, budgetMs: number, awaiting: string): string | undefined;