@reticlehq/server 3.3.0 → 3.4.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 (299) 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 +4 -0
  15. package/dist/command/cli/auth/cloud-session.js +2 -0
  16. package/dist/command/cli/cli-doctor.js +7 -5
  17. package/dist/command/cli/cli-flow-commands.d.ts +3 -6
  18. package/dist/command/cli/cli-flow-commands.js +67 -4
  19. package/dist/command/cli/cli-parse.d.ts +8 -1
  20. package/dist/command/cli/cli-parse.js +21 -3
  21. package/dist/command/cli/cli-update-commands.js +8 -1
  22. package/dist/command/cli/cli-verify.d.ts +24 -3
  23. package/dist/command/cli/cli-verify.js +54 -11
  24. package/dist/command/cli/cloud-cli.js +5 -2
  25. package/dist/command/cli/cloud-kit.js +3 -5
  26. package/dist/command/cli/cloud-login.js +21 -3
  27. package/dist/command/cli/demo/demo-page.js +6 -3
  28. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +5 -0
  29. package/dist/command/cli/doctor/browser/chromium-hint.js +5 -0
  30. package/dist/command/cli/launch/cli-launch.d.ts +8 -0
  31. package/dist/command/cli/launch/cli-launch.js +21 -9
  32. package/dist/command/cli/launch/sdk-version-match.d.ts +84 -0
  33. package/dist/command/cli/launch/sdk-version-match.js +206 -0
  34. package/dist/command/cli/lifecycle/daemon-lifecycle.js +2 -16
  35. package/dist/command/cli/ports/resolve/cli-port.d.ts +25 -0
  36. package/dist/command/cli/ports/resolve/cli-port.js +125 -15
  37. package/dist/command/cli/setup-mcp-cli.js +13 -13
  38. package/dist/command/cli/tutorial.d.ts +2 -2
  39. package/dist/command/cli/tutorial.js +21 -7
  40. package/dist/command/cli.js +62 -10
  41. package/dist/command/daemon/binding/daemon-status-probe.d.ts +14 -2
  42. package/dist/command/daemon/binding/daemon-status-probe.js +34 -6
  43. package/dist/command/daemon/daemon-resolve.d.ts +9 -0
  44. package/dist/command/daemon/daemon-resolve.js +1 -1
  45. package/dist/command/daemon/lifetime/daemon-usefulness.d.ts +7 -1
  46. package/dist/command/daemon/lifetime/daemon-usefulness.js +30 -7
  47. package/dist/command/dev/stale-issue-guard.d.ts +3 -4
  48. package/dist/command/dev/stale-issue-guard.js +8 -8
  49. package/dist/command/setup/bringup/connect-lease.d.ts +17 -0
  50. package/dist/command/setup/bringup/connect-lease.js +38 -0
  51. package/dist/command/setup/bringup/dev-server-files.d.ts +68 -0
  52. package/dist/command/setup/bringup/dev-server-files.js +131 -0
  53. package/dist/command/setup/bringup/dev-server-log-cap.d.ts +35 -0
  54. package/dist/command/setup/bringup/dev-server-log-cap.js +121 -0
  55. package/dist/command/setup/bringup/ensure-daemon.d.ts +65 -4
  56. package/dist/command/setup/bringup/ensure-daemon.js +135 -16
  57. package/dist/command/setup/bringup/static-page-server.d.ts +35 -0
  58. package/dist/command/setup/bringup/static-page-server.js +151 -0
  59. package/dist/command/setup/desktop-shape.d.ts +4 -0
  60. package/dist/command/setup/desktop-shape.js +18 -2
  61. package/dist/command/setup/init/init-port.d.ts +15 -0
  62. package/dist/command/setup/init/init-port.js +44 -0
  63. package/dist/command/setup/init/init-runtime.d.ts +1 -1
  64. package/dist/command/setup/init/init-runtime.js +39 -2
  65. package/dist/command/setup/node-effects.d.ts +27 -14
  66. package/dist/command/setup/node-effects.js +80 -44
  67. package/dist/command/setup/probe/loopback-probe-urls.d.ts +7 -0
  68. package/dist/command/setup/probe/loopback-probe-urls.js +40 -0
  69. package/dist/command/setup/probe/page-probe.d.ts +13 -2
  70. package/dist/command/setup/probe/page-probe.js +18 -3
  71. package/dist/command/setup/run-setup.d.ts +26 -1
  72. package/dist/command/setup/run-setup.js +96 -13
  73. package/dist/command/setup/session-pick.d.ts +13 -0
  74. package/dist/command/setup/session-pick.js +20 -0
  75. package/dist/command/setup/setup-command.d.ts +2 -14
  76. package/dist/command/setup/setup-command.js +105 -31
  77. package/dist/command/setup/setup-mcp.js +3 -3
  78. package/dist/command/setup/terminal/wait-progress.d.ts +8 -0
  79. package/dist/command/setup/terminal/wait-progress.js +17 -0
  80. package/dist/features/crawl/crawl-empty.js +1 -1
  81. package/dist/features/crawl/crawl-tools.d.ts +6 -0
  82. package/dist/features/crawl/crawl-tools.js +57 -0
  83. package/dist/features/crawl/crawl.js +171 -157
  84. package/dist/features/exhaust/app-coverage.d.ts +33 -0
  85. package/dist/features/exhaust/app-coverage.js +31 -0
  86. package/dist/features/exhaust/code-coverage.d.ts +47 -0
  87. package/dist/features/exhaust/code-coverage.js +77 -0
  88. package/dist/features/exhaust/exhaust-run.d.ts +33 -0
  89. package/dist/features/exhaust/exhaust-run.js +82 -0
  90. package/dist/features/exhaust/explorer.d.ts +103 -0
  91. package/dist/features/exhaust/explorer.js +195 -0
  92. package/dist/features/exhaust/failure-branches.d.ts +33 -0
  93. package/dist/features/exhaust/failure-branches.js +92 -0
  94. package/dist/features/exhaust/ledger.d.ts +202 -0
  95. package/dist/features/exhaust/ledger.js +252 -0
  96. package/dist/features/exhaust/session-fold.d.ts +31 -0
  97. package/dist/features/exhaust/session-fold.js +79 -0
  98. package/dist/features/exhaust/session-port.d.ts +36 -0
  99. package/dist/features/exhaust/session-port.js +102 -0
  100. package/dist/features/harness/drive-report.d.ts +0 -9
  101. package/dist/features/harness/drive-report.js +18 -0
  102. package/dist/features/harness/driver.js +4 -7
  103. package/dist/features/harness/fill-values.d.ts +6 -0
  104. package/dist/features/harness/fill-values.js +3 -0
  105. package/dist/features/harness/jev-driver.js +30 -13
  106. package/dist/features/harness/openai-driver.js +4 -7
  107. package/dist/features/harness/platform-config.js +5 -6
  108. package/dist/index.d.ts +9 -0
  109. package/dist/index.js +40 -11
  110. package/dist/judgement/domain/domain-model.js +3 -1
  111. package/dist/judgement/oracles/self-instrument.js +4 -2
  112. package/dist/judgement/runs/artifact/replay-mapping.d.ts +2 -2
  113. package/dist/judgement/runs/artifact/replay-mapping.js +44 -9
  114. package/dist/judgement/runs/verification-sync.d.ts +3 -1
  115. package/dist/judgement/runs/verification-sync.js +1 -1
  116. package/dist/language/flows/change/affected.d.ts +9 -0
  117. package/dist/language/flows/change/affected.js +35 -0
  118. package/dist/language/flows/change/gate.d.ts +20 -0
  119. package/dist/language/flows/change/gate.js +12 -1
  120. package/dist/language/flows/flow-author.d.ts +7 -0
  121. package/dist/language/flows/flow-author.js +19 -0
  122. package/dist/language/flows/flow-expect-grammar.js +1 -1
  123. package/dist/language/flows/flow-journey.d.ts +54 -0
  124. package/dist/language/flows/flow-journey.js +119 -0
  125. package/dist/language/flows/flow-replay-run.d.ts +2 -3
  126. package/dist/language/flows/flow-replay-run.js +13 -47
  127. package/dist/language/flows/flow-replay.d.ts +22 -8
  128. package/dist/language/flows/flow-replay.js +55 -9
  129. package/dist/language/flows/flow-result.d.ts +2 -0
  130. package/dist/language/flows/flow-result.js +25 -0
  131. package/dist/language/flows/flow-tools.js +17 -5
  132. package/dist/language/flows/flows.d.ts +10 -1
  133. package/dist/language/flows/flows.js +90 -14
  134. package/dist/language/flows/recording/replay-halt.js +3 -1
  135. package/dist/language/flows/recording/tape/recordings.d.ts +30 -1
  136. package/dist/language/flows/recording/tape/recordings.js +55 -1
  137. package/dist/language/flows/replay.d.ts +19 -0
  138. package/dist/language/flows/replay.js +42 -6
  139. package/dist/language/flows/verify-change-tools.js +5 -2
  140. package/dist/launch-chromium.d.ts +80 -0
  141. package/dist/launch-chromium.js +218 -0
  142. package/dist/memory/cloud/cloud-config.d.ts +11 -2
  143. package/dist/memory/cloud/cloud-config.js +47 -7
  144. package/dist/memory/cloud/cloud-sync.d.ts +4 -1
  145. package/dist/memory/cloud/cloud-sync.js +6 -9
  146. package/dist/memory/cloud/harness-offer.js +5 -6
  147. package/dist/memory/cloud/harness-switch.js +5 -6
  148. package/dist/memory/cloud/sync-cycle.d.ts +68 -3
  149. package/dist/memory/cloud/sync-cycle.js +298 -55
  150. package/dist/memory/cloud/sync-daemon.js +24 -20
  151. package/dist/memory/cloud/sync-disk.js +16 -2
  152. package/dist/memory/intent/intent-shard.d.ts +10 -32
  153. package/dist/memory/intent/intent-shard.js +0 -10
  154. package/dist/memory/journal/drive-flow.d.ts +18 -13
  155. package/dist/memory/journal/drive-flow.js +45 -37
  156. package/dist/memory/journal/on-disk/workspace-tiers.js +2 -0
  157. package/dist/memory/journal/session-end.d.ts +9 -1
  158. package/dist/memory/journal/session-end.js +24 -12
  159. package/dist/memory/project/dir/reticle-dir.d.ts +2 -0
  160. package/dist/memory/project/dir/reticle-dir.js +1 -0
  161. package/dist/memory/project/project-tools.js +3 -1
  162. package/dist/portal/bridge/auth-failure-reason.d.ts +1 -1
  163. package/dist/portal/bridge/auth-failure-reason.js +4 -2
  164. package/dist/portal/bridge/bridge.d.ts +8 -0
  165. package/dist/portal/bridge/bridge.js +43 -2
  166. package/dist/portal/input/js-coverage.d.ts +25 -0
  167. package/dist/portal/input/js-coverage.js +30 -0
  168. package/dist/portal/input/real-input.d.ts +11 -0
  169. package/dist/portal/input/real-input.js +29 -4
  170. package/dist/portal/pool/playwright-launcher.d.ts +6 -0
  171. package/dist/portal/pool/playwright-launcher.js +20 -13
  172. package/dist/portal/session/dev-server/dev-command.d.ts +2 -2
  173. package/dist/portal/session/dev-server/dev-command.js +30 -12
  174. package/dist/portal/session/dev-server/dev-server-probe.js +17 -1
  175. package/dist/portal/session/dev-server/route-status-probe.d.ts +24 -0
  176. package/dist/portal/session/dev-server/route-status-probe.js +57 -0
  177. package/dist/portal/session/facts/observed-state.d.ts +11 -0
  178. package/dist/portal/session/facts/observed-state.js +34 -0
  179. package/dist/portal/session/no-session-diagnosis.d.ts +29 -0
  180. package/dist/portal/session/no-session-diagnosis.js +62 -5
  181. package/dist/portal/session/no-session-next-action.d.ts +13 -0
  182. package/dist/portal/session/no-session-next-action.js +48 -9
  183. package/dist/portal/session/no-session-watch.d.ts +18 -0
  184. package/dist/portal/session/no-session-watch.js +88 -2
  185. package/dist/portal/session/session-health.js +3 -2
  186. package/dist/portal/session/session-manager.d.ts +20 -6
  187. package/dist/portal/session/session-manager.js +19 -3
  188. package/dist/portal/session/session-reaper.d.ts +28 -0
  189. package/dist/portal/session/session-reaper.js +49 -1
  190. package/dist/portal/session/session.d.ts +5 -0
  191. package/dist/portal/session/session.js +15 -19
  192. package/dist/surface/http-server.d.ts +2 -1
  193. package/dist/surface/http-server.js +4 -2
  194. package/dist/surface/mcp/mcp-proxy.d.ts +2 -2
  195. package/dist/surface/mcp/mcp-proxy.js +2 -2
  196. package/dist/surface/mcp/mcp.js +4 -2
  197. package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +13 -1
  198. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +27 -2
  199. package/dist/surface/mcp/server-instructions.js +9 -7
  200. package/dist/surface/tools/act/after-match.d.ts +17 -0
  201. package/dist/surface/tools/act/after-match.js +30 -0
  202. package/dist/surface/tools/act/durable.d.ts +69 -0
  203. package/dist/surface/tools/act/durable.js +139 -0
  204. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +13 -6
  205. package/dist/surface/tools/act/navigation/navigate-arrival.js +47 -10
  206. package/dist/surface/tools/act/navigation/navigate-result.d.ts +1 -12
  207. package/dist/surface/tools/act/navigation/navigate-result.js +21 -3
  208. package/dist/surface/tools/act/proved-controls.d.ts +9 -0
  209. package/dist/surface/tools/act/proved-controls.js +11 -0
  210. package/dist/surface/tools/act/sequence-grade.d.ts +5 -0
  211. package/dist/surface/tools/act/sequence-grade.js +32 -1
  212. package/dist/surface/tools/act-capsule.d.ts +7 -0
  213. package/dist/surface/tools/act-capsule.js +46 -1
  214. package/dist/surface/tools/act-sequence-tool.js +25 -8
  215. package/dist/surface/tools/act-tools.js +26 -50
  216. package/dist/surface/tools/assert/assert-grade.js +12 -1
  217. package/dist/surface/tools/assert/assert-verdict.js +2 -2
  218. package/dist/surface/tools/browser-tools.js +14 -2
  219. package/dist/surface/tools/coverage-tools.js +27 -1
  220. package/dist/surface/tools/error-recovery.js +4 -4
  221. package/dist/surface/tools/feedback-invite.js +6 -4
  222. package/dist/surface/tools/harness-explore.d.ts +3 -2
  223. package/dist/surface/tools/harness-explore.js +7 -6
  224. package/dist/surface/tools/lease-tools.js +15 -6
  225. package/dist/surface/tools/live-call-text.js +4 -1
  226. package/dist/surface/tools/observe-tools.js +9 -1
  227. package/dist/surface/tools/tools.js +4 -0
  228. package/dist/telemetry/argument-shape.d.ts +0 -2
  229. package/dist/telemetry/argument-shape.js +0 -4
  230. package/dist/telemetry/error-class.d.ts +2 -3
  231. package/dist/telemetry/error-class.js +2 -3
  232. package/dist/telemetry/harness-drive.d.ts +0 -2
  233. package/dist/telemetry/harness-drive.js +0 -4
  234. package/dist/wire-journal.d.ts +3 -0
  235. package/dist/wire-journal.js +4 -0
  236. package/dist/workspace-packages.d.ts +0 -2
  237. package/dist/workspace-packages.js +1 -9
  238. package/docs/README.md +4 -2
  239. package/docs/actions.mdx +3 -3
  240. package/docs/agent-cheatsheet.md +4 -4
  241. package/docs/architecture.md +6 -6
  242. package/docs/best-practices.mdx +1 -1
  243. package/docs/capabilities.mdx +32 -1
  244. package/docs/cli/cloud.mdx +14 -4
  245. package/docs/cli/doctor.mdx +4 -0
  246. package/docs/cli/feedback.mdx +2 -2
  247. package/docs/cli/gate.mdx +9 -4
  248. package/docs/cli/update.mdx +10 -0
  249. package/docs/cli.mdx +9 -0
  250. package/docs/desktop.mdx +14 -10
  251. package/docs/environment.mdx +87 -0
  252. package/docs/faq.mdx +18 -16
  253. package/docs/first-drive.md +5 -13
  254. package/docs/flows.md +21 -2
  255. package/docs/frameworks.mdx +29 -10
  256. package/docs/getting-started.md +3 -3
  257. package/docs/http-transport.md +6 -3
  258. package/docs/human-control.md +2 -2
  259. package/docs/index.mdx +16 -6
  260. package/docs/install-agentic.mdx +1 -1
  261. package/docs/install-manual.mdx +1 -1
  262. package/docs/integration-patterns.md +3 -3
  263. package/docs/multi-agent-testing.md +1 -1
  264. package/docs/packages/babel-plugin.mdx +1 -1
  265. package/docs/packages/browser.mdx +2 -2
  266. package/docs/packages/react.mdx +2 -2
  267. package/docs/packages/tauri.mdx +1 -1
  268. package/docs/packages/vite-plugin.mdx +2 -2
  269. package/docs/packages.mdx +9 -9
  270. package/docs/predicates.mdx +50 -5
  271. package/docs/quickstart.mdx +2 -2
  272. package/docs/recipes.mdx +2 -2
  273. package/docs/state-management.mdx +2 -2
  274. package/docs/system-map.md +1 -1
  275. package/docs/telemetry.md +1 -1
  276. package/docs/token-efficiency.md +3 -3
  277. package/docs/tools/act-and-wait.mdx +1 -0
  278. package/docs/tools/act-sequence.mdx +1 -1
  279. package/docs/tools/console.mdx +1 -1
  280. package/docs/tools/inspect.mdx +1 -1
  281. package/docs/tools/network.mdx +1 -1
  282. package/docs/tools/query.mdx +1 -1
  283. package/docs/tools/sessions.mdx +1 -1
  284. package/docs/tools/snapshot.mdx +1 -1
  285. package/docs/tools/state.mdx +1 -1
  286. package/docs/tools/wait-for.mdx +1 -1
  287. package/docs/usage.md +7 -6
  288. package/docs/use-cases.mdx +157 -0
  289. package/docs/vs-screenshots.mdx +3 -3
  290. package/docs/what-is-recorded.md +2 -2
  291. package/docs/what-is-reticle.mdx +17 -20
  292. package/docs/why-reticle.mdx +14 -21
  293. package/package.json +10 -9
  294. package/dist/judgement/capsule/minimize.d.ts +0 -12
  295. package/dist/judgement/capsule/minimize.js +0 -22
  296. package/dist/judgement/phenomena/phenomena.d.ts +0 -27
  297. package/dist/judgement/phenomena/phenomena.js +0 -101
  298. package/dist/language/flows/flow-report.d.ts +0 -22
  299. package/dist/language/flows/flow-report.js +0 -106
@@ -50,3 +50,43 @@ export function loopbackProbeUrls(url) {
50
50
  push(host);
51
51
  return out;
52
52
  }
53
+ /**
54
+ * Errors that mean "nothing is listening at THIS address", so another loopback family may be.
55
+ *
56
+ * Anything else, a timeout above all, means the address was right and the server is slow. A cold
57
+ * Next 16 first compile takes long enough to time a probe out, and treating that as a family miss
58
+ * walked on to `127.0.0.1`/`[::1]`, which answered first, became the app url, and is an origin Next
59
+ * refuses its dev resources to: no hydration, so the SDK never ran and init blamed the page.
60
+ */
61
+ const ADDRESS_MISS_CODES = new Set([
62
+ 'ECONNREFUSED',
63
+ 'EADDRNOTAVAIL',
64
+ 'ENOTFOUND',
65
+ 'EAI_AGAIN',
66
+ 'ENETUNREACH',
67
+ 'EHOSTUNREACH',
68
+ 'EAFNOSUPPORT',
69
+ ]);
70
+ function field(value, key) {
71
+ return 'object' === typeof value && null !== value ? Reflect.get(value, key) : undefined;
72
+ }
73
+ const codeOf = (value) => {
74
+ const code = field(value, 'code');
75
+ return 'string' === typeof code ? code : undefined;
76
+ };
77
+ /**
78
+ * Whether a failed fetch was refused at the connection level on this address.
79
+ *
80
+ * `fetch` wraps the socket error as `cause`; with happy-eyeballs that cause is an AggregateError of
81
+ * one error per address tried, and it is a miss only when every one of them was.
82
+ */
83
+ export function isAddressMiss(err) {
84
+ const cause = field(err, 'cause');
85
+ const direct = codeOf(cause);
86
+ if (undefined !== direct)
87
+ return ADDRESS_MISS_CODES.has(direct);
88
+ const errors = field(cause, 'errors');
89
+ if (!Array.isArray(errors) || 0 === errors.length)
90
+ return false;
91
+ return errors.every((e) => ADDRESS_MISS_CODES.has(codeOf(e) ?? ''));
92
+ }
@@ -40,8 +40,13 @@ export declare function readPage(probe: PageProbe): PageFinding;
40
40
  /**
41
41
  * What the page adds to the daemon's account. Deliberately a sentence to APPEND, not a replacement:
42
42
  * the daemon's diagnosis names the fix, and this says what the page looked like when we checked.
43
+ *
44
+ * `daemonSpoke`: the daemon already gave its account. Whether the page dialled is ITS fact (it
45
+ * sees every hello, including one it refused), so the SDK_PRESENT sentence then states only the
46
+ * page fact. Saying "never dialled the bridge" under "this daemon REFUSED the last page" contradicted
47
+ * the one piece of positive evidence in the run.
43
48
  */
44
- export declare function describePage(finding: PageFinding, url: string): string;
49
+ export declare function describePage(finding: PageFinding, url: string, daemonSpoke?: boolean): string;
45
50
  /**
46
51
  * What the page looks like once it has had its short chance to come up.
47
52
  *
@@ -68,4 +73,10 @@ export declare function describePage(finding: PageFinding, url: string): string;
68
73
  export declare function findingBeforeOpen(probe: () => Promise<PageProbe>, clock: {
69
74
  readonly now: () => number;
70
75
  readonly sleep: (ms: number) => Promise<void>;
71
- }, windowMs: number, pollMs: number): Promise<PageFinding>;
76
+ }, windowMs: number, pollMs: number,
77
+ /**
78
+ * The served HTML carries the SDK marker on this shape. When it does not (the connect is in the
79
+ * JS bundle), SDK_MISSING is the permanent state of a healthy page, and waiting for it to change
80
+ * only delays the window that would load the bundle.
81
+ */
82
+ htmlCarriesSdk?: boolean): Promise<PageFinding>;
@@ -27,8 +27,16 @@ export function readPage(probe) {
27
27
  /**
28
28
  * What the page adds to the daemon's account. Deliberately a sentence to APPEND, not a replacement:
29
29
  * the daemon's diagnosis names the fix, and this says what the page looked like when we checked.
30
+ *
31
+ * `daemonSpoke`: the daemon already gave its account. Whether the page dialled is ITS fact (it
32
+ * sees every hello, including one it refused), so the SDK_PRESENT sentence then states only the
33
+ * page fact. Saying "never dialled the bridge" under "this daemon REFUSED the last page" contradicted
34
+ * the one piece of positive evidence in the run.
30
35
  */
31
- export function describePage(finding, url) {
36
+ export function describePage(finding, url, daemonSpoke = false) {
37
+ if (PageFinding.SDK_PRESENT === finding && daemonSpoke) {
38
+ return `The SDK IS in the page at ${url}; the daemon's account above says what became of it.`;
39
+ }
32
40
  switch (finding) {
33
41
  case PageFinding.NOT_SERVED:
34
42
  // Leads with the cause. The break-matrix asserts this phrasing because it is what a reader
@@ -77,10 +85,17 @@ export function describePage(finding, url) {
77
85
  * `probePage` 1,500 times before reaching this point and a test of the decision cannot get near
78
86
  * it through that. Here it takes a probe and a clock and answers in three lines of setup.
79
87
  */
80
- export async function findingBeforeOpen(probe, clock, windowMs, pollMs) {
88
+ export async function findingBeforeOpen(probe, clock, windowMs, pollMs,
89
+ /**
90
+ * The served HTML carries the SDK marker on this shape. When it does not (the connect is in the
91
+ * JS bundle), SDK_MISSING is the permanent state of a healthy page, and waiting for it to change
92
+ * only delays the window that would load the bundle.
93
+ */
94
+ htmlCarriesSdk = true) {
81
95
  const readyBy = clock.now() + windowMs;
82
96
  let finding = readPage(await probe());
83
- while ((PageFinding.SDK_MISSING === finding || PageFinding.NOT_SERVED === finding) &&
97
+ while (((htmlCarriesSdk && PageFinding.SDK_MISSING === finding) ||
98
+ PageFinding.NOT_SERVED === finding) &&
84
99
  clock.now() < readyBy) {
85
100
  await clock.sleep(pollMs);
86
101
  finding = readPage(await probe());
@@ -51,6 +51,11 @@ export interface DaemonReason {
51
51
  /** The whole differential, recorded for the agent reading `--json`. Defaults to the lead. */
52
52
  readonly full?: string;
53
53
  }
54
+ /** A lease this run opened for its connect proof, and the way to hand it back. */
55
+ export interface OwnedBrowser {
56
+ readonly sessionId: string;
57
+ readonly release: () => Promise<void>;
58
+ }
54
59
  export interface SetupEffects {
55
60
  /** Start the dev server. Resolves once started; the caller owns stopping it. */
56
61
  readonly startDevServer: (command: string, cwd: string) => Promise<void>;
@@ -79,8 +84,19 @@ export interface SetupEffects {
79
84
  * short (`vite-react`, `vite-vue`, `next-app-router`, `monorepo-subdir`, run 35242186243 against
80
85
  * 35232727716). A non-zero launcher exit means UNKNOWN, not "no browser": it reports whether the
81
86
  * COMMAND succeeded, never whether a window opened. Do not shorten a wait on it.
87
+ *
88
+ * It may resolve with the launcher's own failure, and that is used for exactly one thing: to ALSO
89
+ * open a Reticle-owned browser, so the proof does not depend on a window that may never appear.
90
+ * Adding a second source of a session is safe on a false alarm; cutting the wait was not.
91
+ */
92
+ readonly openBrowser: (url: string) => Promise<string | void>;
93
+ /**
94
+ * Open the url in a browser Reticle owns (a pooled lease), for a run with no system browser to
95
+ * lean on: `--no-open`, CI, a container, an agent. Optional: absent means no lease is attempted.
82
96
  */
83
- readonly openBrowser: (url: string) => Promise<void>;
97
+ readonly openLease?: (url: string) => Promise<OwnedBrowser | {
98
+ readonly failed: string;
99
+ }>;
84
100
  readonly listSessions: () => Promise<CandidateSession[]>;
85
101
  /**
86
102
  * The daemon's own account of why nothing connected, if it can be asked.
@@ -101,6 +117,8 @@ export interface SetupEffects {
101
117
  }
102
118
  export interface SetupInput {
103
119
  readonly appDir: string;
120
+ /** This project's id, so a live tab of it can be told from another app's on the same url. */
121
+ readonly projectId?: string | undefined;
104
122
  readonly devCommand?: string | undefined;
105
123
  /** The app is already served here, so nothing is started. */
106
124
  readonly suppliedUrl?: string | undefined;
@@ -117,6 +135,13 @@ export interface SetupInput {
117
135
  readonly openBrowser: boolean;
118
136
  /** Web, Electron or Tauri. Desktop changes three things; see desktop-shape.ts. */
119
137
  readonly shape: AppShape;
138
+ /**
139
+ * The served HTML carries the SDK marker on this shape: Vite's index.html injection, or a plain
140
+ * HTML page with the snippet pasted in. Every other framework delivers the connect in the JS
141
+ * bundle, so "the SDK is NOT in the page" is the permanent state of a healthy page there, and
142
+ * saying it sent people to restart a dev server that was fine.
143
+ */
144
+ readonly htmlCarriesSdk: boolean;
120
145
  readonly phaseTimeoutMs: number;
121
146
  readonly pollMs: number;
122
147
  }
@@ -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
  };
@@ -181,9 +201,13 @@ export async function runSetupPhases(input, fx) {
181
201
  const requiredRuntime = isDesktop(input.shape) ? input.shape : undefined;
182
202
  // A tab already on this URL is the answer. Opening another is how three sessions appear.
183
203
  // `before` is empty so pickSession will take that tab rather than waiting for a new one.
204
+ //
205
+ // Not only when attached: a server init started itself (Next, Angular — nothing announces) has a
206
+ // url too, and a live tab of this project already on it is just as much the answer. Without this
207
+ // every re-run opened one more tab.
184
208
  const alreadyConnected = attachedToExisting
185
209
  ? pickSession(listed, url, new Set(), requiredRuntime)
186
- : null;
210
+ : liveSessionOfProject(listed, url, input.projectId, requiredRuntime);
187
211
  const before = new Set(listed.map((s) => s.sessionId));
188
212
  // Do not put a window in front of somebody until the page behind it can actually do something.
189
213
  //
@@ -198,7 +222,12 @@ export async function runSetupPhases(input, fx) {
198
222
  // proof that nothing can ever dial in.
199
223
  const budgetMs = input.connectBudgetMs ?? Math.max(input.phaseTimeoutMs, policy.connectBudgetMs);
200
224
  let openedBrowser = false;
201
- if (null === alreadyConnected && input.openBrowser && policy.openBrowser) {
225
+ let lease;
226
+ // `--no-open` used to skip this whole block, so nothing was opened and the run ended on "never
227
+ // dialled the bridge" over a correct install. With a lease to fall back on it is entered too, and
228
+ // only the system browser is skipped.
229
+ const canLease = undefined !== fx.openLease;
230
+ if (null === alreadyConnected && policy.openBrowser && (input.openBrowser || canLease)) {
202
231
  // SERVED is the precondition, not SDK_PRESENT: a url that answers nothing is the only state
203
232
  // where a window is certainly useless. Whether the SDK is in the served HTML is a DIFFERENT
204
233
  // question — Vite injects a marker, while Nuxt, React Router, Astro, SvelteKit and CRA deliver
@@ -209,7 +238,7 @@ export async function runSetupPhases(input, fx) {
209
238
  //
210
239
  // So presence decides only WHEN: open the moment it appears (the fast path for the frameworks
211
240
  // 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);
241
+ 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
242
  if (PageFinding.NOT_SERVED === finding || PageFinding.TLS_REFUSED === finding) {
214
243
  note(describePage(finding, url));
215
244
  note('Not opening a browser: nothing answered that url, so the window could only show an error ' +
@@ -218,12 +247,23 @@ export async function runSetupPhases(input, fx) {
218
247
  else {
219
248
  // Said BEFORE the window appears, so somebody watching a page that stays inert has already
220
249
  // been told which of the two it is.
221
- if (PageFinding.SDK_MISSING === finding)
250
+ if (PageFinding.SDK_MISSING === finding && input.htmlCarriesSdk) {
222
251
  note(describePage(finding, url));
252
+ }
223
253
  // `= true` means ATTEMPTED, which is the most that is knowable — see openBrowser above for
224
254
  // why the launcher's own exit code must not be read as "no window appeared".
225
- await fx.openBrowser(url);
226
- openedBrowser = true;
255
+ const launcherFailed = input.openBrowser ? await fx.openBrowser(url) : undefined;
256
+ openedBrowser = input.openBrowser;
257
+ if (undefined !== fx.openLease && (!input.openBrowser || undefined !== launcherFailed)) {
258
+ const owned = await fx.openLease(url);
259
+ if ('failed' in owned) {
260
+ note(`Could not open ${url} in a Reticle-owned browser either: ${owned.failed}`);
261
+ }
262
+ else {
263
+ lease = owned;
264
+ openedBrowser = true;
265
+ }
266
+ }
227
267
  }
228
268
  }
229
269
  // Waiting the full budget for a session when nothing was opened to create one is dead time, and
@@ -237,6 +277,9 @@ export async function runSetupPhases(input, fx) {
237
277
  ? budgetMs
238
278
  : Math.min(NO_BROWSER_GRACE_MS, budgetMs));
239
279
  let session = alreadyConnected;
280
+ const waitStartedAt = fx.now();
281
+ let connectSpokeAtMs;
282
+ let leaseFallbackTried = false;
240
283
  while (null === session) {
241
284
  // On a desktop app, only the desktop window counts. AppShape and the runtime a page reports use
242
285
  // the same three names, so the shape IS the requirement — see session-pick.
@@ -245,6 +288,32 @@ export async function runSetupPhases(input, fx) {
245
288
  break;
246
289
  if (deadline <= fx.now())
247
290
  break;
291
+ if (!leaseFallbackTried &&
292
+ undefined === lease &&
293
+ undefined !== fx.openLease &&
294
+ openedBrowser &&
295
+ input.openBrowser &&
296
+ !isDesktop(input.shape) &&
297
+ fx.now() - waitStartedAt >= SYSTEM_BROWSER_GRACE_MS) {
298
+ leaseFallbackTried = true;
299
+ const owned = await fx.openLease(url);
300
+ if ('failed' in owned) {
301
+ note(`Could not open ${url} in a Reticle-owned browser either: ${owned.failed}`);
302
+ }
303
+ else {
304
+ lease = owned;
305
+ }
306
+ continue;
307
+ }
308
+ // Not in silence: a desktop run used to print one line and then nothing for ten minutes.
309
+ const waitedMs = fx.now() - waitStartedAt;
310
+ const progress = connectProgressLine(waitedMs, connectSpokeAtMs, deadline - waitStartedAt, policy.awaiting);
311
+ if (undefined !== progress) {
312
+ // To the person only: it is progress, not a finding, and in `notes` it would split the
313
+ // page description printed before and after this wait into two copies.
314
+ fx.note(progress);
315
+ connectSpokeAtMs = waitedMs;
316
+ }
248
317
  await fx.sleep(input.pollMs);
249
318
  }
250
319
  if (null === session) {
@@ -275,8 +344,13 @@ export async function runSetupPhases(input, fx) {
275
344
  fx.note(why.lead);
276
345
  notes.push(why.full ?? why.lead);
277
346
  }
278
- note(describePage(readPage(await fx.probePage(url)), url));
347
+ const finding = readPage(await fx.probePage(url));
348
+ // Silent on a bundle-connect shape: its HTML never carries the SDK, so the absence says nothing.
349
+ if (PageFinding.SDK_MISSING !== finding || input.htmlCarriesSdk) {
350
+ note(describePage(finding, url, undefined !== why));
351
+ }
279
352
  }
353
+ await releaseLease(lease);
280
354
  return stop(input, SetupPhase.CONNECT, { url }, notes);
281
355
  }
282
356
  // ── connected, which is where ONBOARDING ends ────────────────────────────────────────────────
@@ -292,16 +366,25 @@ export async function runSetupPhases(input, fx) {
292
366
  // Both routes, because `explore` needs a model: without ANTHROPIC_API_KEY it answers "No model
293
367
  // configured to drive the app". Naming only that one hands the reader a dead end on any machine
294
368
  // 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.
369
+ // A heading and a numbered list rather than one paragraph: a reader scans for what to do next.
296
370
  //
297
371
  // This was a single 524-character note, measured on a real first run: eighty-three words, no
298
372
  // break, naming `reticle_*` tools at a reader who had just typed `reticle init` in a terminal and
299
373
  // has no such tools to call. The facts were right and the shape made them unreadable, which on
300
374
  // 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 ' +
375
+ note(`✓ Connected. ${url} is instrumented. Onboarding is done; nothing is verified yet.`);
376
+ // Said, because "connected" otherwise reads as "your browser connected": the page that dialled
377
+ // was a headless one Reticle launched and has now closed, and nothing is open on this screen.
378
+ if (undefined !== lease && session.sessionId === lease.sessionId) {
379
+ note('The proof came from a Reticle-owned headless browser (now closed), not a window of yours — ' +
380
+ `open ${url} in your own browser to use the app.`);
381
+ }
382
+ await releaseLease(lease);
383
+ note('');
384
+ note('Next, prove one flow. Your agent does this:');
385
+ note(' 1. Drive one flow and end it with `reticle_act_and_wait` or `reticle_assert`. Those two ' +
303
386
  'produce a verdict; nothing else does.');
304
- note('Or hand over the whole drive: `reticle_verify { action: "explore", persona: "<who does ' +
387
+ note(' 2. Or hand over the whole drive: `reticle_verify { action: "explore", persona: "<who does ' +
305
388
  'what>" }` records what it drove, so later runs replay with no model in the loop. Needs ' +
306
389
  'ANTHROPIC_API_KEY.');
307
390
  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 {};