@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
@@ -6,19 +6,30 @@
6
6
  * every command harder to read.
7
7
  */
8
8
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
9
- import { RETICLE_DEFAULT_PORT } from '@reticlehq/core';
9
+ import { InitConfirmation, RETICLE_DEFAULT_PORT } from '@reticlehq/core';
10
+ import { FEEDBACK_HINT, Framework, InitFailure } from '@reticlehq/init';
10
11
  import { confirmInstall, nodeConfirmDeps } from '../terminal/confirm.js';
11
12
  import { writeLicenseKey } from '../license-key.js';
12
13
  import { registerOtherAgents, runSetupCommand } from '../setup-command.js';
13
14
  import { bridgeOccupied } from '../bringup/bridge-port.js';
15
+ import { defaultPairingTokenDir, readOrCreatePairingTokenSync, } from '../../../portal/bridge/pairing-token.js';
14
16
  import { relaunchDecision } from '../bringup/relaunch.js';
15
17
  import { claudeTranscriptExists, codexSessionFor } from '../terminal/transcripts.js';
16
18
  import { probePresence } from '../../daemon/binding/port-presence.js';
17
19
  import { probeDaemon } from '../../../surface/mcp/proxy/proxy-daemon-probe.js';
18
20
  import { fetchStatus } from '../../daemon/binding/daemon-status-probe.js';
19
21
  import { collectEnv, DEFAULT_PHASE_TIMEOUT_MS } from '../setup-options.js';
22
+ import { staticPageDevCommand } from '../bringup/static-page-server.js';
23
+ import { SetupPhase } from '../run-setup.js';
24
+ import { reportInitOutcome } from '../../../telemetry/init-telemetry.js';
20
25
  /** How often the runtime phases look again: fast enough not to be the wait, slow enough to be free. */
21
26
  const POLL_MS = 250;
27
+ /**
28
+ * The shapes whose served HTML carries the SDK marker: Vite injects it into index.html, and a plain
29
+ * HTML page has the snippet pasted in. Everything else connects from the JS bundle, where a fetch
30
+ * of the document can never see it. See `htmlCarriesSdk` in run-setup.ts.
31
+ */
32
+ const HTML_CARRIES_SDK = new Set([Framework.VITE, Framework.HTML]);
22
33
  /** Registration is global MCP wiring, so both flags that disown it are read here. */
23
34
  function wantsAgents(parsed) {
24
35
  return false !== parsed.agents && false !== parsed.mcp;
@@ -50,9 +61,9 @@ function printRelaunch(parsed, io, cwd) {
50
61
  }
51
62
  export async function continueAfterInit(parsed, result, io, cwd) {
52
63
  const port = parsed.port ?? RETICLE_DEFAULT_PORT;
53
- // Before anything else: the key belongs in .env whichever way this run ends, and the CLI folds a
54
- // project-local .env into the environment on every invocation.
55
- if (undefined !== parsed.licenseKey) {
64
+ // Real installs persist the key whichever way setup ends. A preview must leave .env and
65
+ // .gitignore untouched, just as it leaves project wiring and agent configuration untouched.
66
+ if (!parsed.dryRun && undefined !== parsed.licenseKey) {
56
67
  const written = writeLicenseKey(cwd, parsed.licenseKey, licenseIo);
57
68
  io.print(written.message);
58
69
  }
@@ -70,18 +81,35 @@ export async function continueAfterInit(parsed, result, io, cwd) {
70
81
  // has: nothing reaches back into a machine that installed Reticle a version ago, so the
71
82
  // upgrade path is re-running init, and the light form of init has to be enough to carry it.
72
83
  // A dry run writes nothing anywhere, including here.
73
- if (true === parsed.filesOnly && wantsAgents(parsed))
84
+ if (!parsed.dryRun && true === parsed.filesOnly && wantsAgents(parsed)) {
74
85
  registerOtherAgents(io.print);
86
+ }
75
87
  return confirmInstall(result, io, nodeConfirmDeps(port)).then(() => {
88
+ if (true === parsed.json)
89
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
76
90
  if (!result.ok)
77
91
  process.exit(1);
78
92
  });
79
93
  }
94
+ // From here on init left the ask to us, so it is the last thing on every exit below.
95
+ const ask = (spaced = true) => {
96
+ if (spaced)
97
+ io.print('');
98
+ io.print(FEEDBACK_HINT);
99
+ };
80
100
  const context = result.context;
81
101
  if (context === undefined) {
82
102
  // Nothing was established, so there is nothing to run against. init has already said why.
103
+ ask();
104
+ if (true === parsed.json)
105
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
83
106
  process.exit(1);
84
107
  }
108
+ const reportFailure = (reason) => {
109
+ if (result.outcome !== undefined) {
110
+ reportInitOutcome({ ...result.outcome, ok: false, reason });
111
+ }
112
+ };
85
113
  // A PENDING connect step is not a reason to skip the runtime phase, and treating it as one made
86
114
  // `init` stop with "paste this snippet" while never looking at the app.
87
115
  //
@@ -97,15 +125,26 @@ export async function continueAfterInit(parsed, result, io, cwd) {
97
125
  // Before the connect wait, never after: a bridge held by a stranger makes a session impossible,
98
126
  // so going ahead spends the entire budget and then reports what reads as an instrumentation
99
127
  // problem — the one place that is fine. See bridge-port.ts.
100
- const refusal = bridgeOccupied(await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus }), port);
128
+ const refusal = bridgeOccupied(await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus }).catch((error) => {
129
+ reportFailure(InitFailure.RUNTIME_ERROR);
130
+ throw error;
131
+ }), port);
101
132
  if (refusal !== undefined) {
133
+ reportFailure(InitFailure.BRIDGE_OCCUPIED);
102
134
  io.print(refusal);
135
+ ask();
136
+ if (true === parsed.json) {
137
+ process.stdout.write(`${JSON.stringify({ ok: false, reason: InitFailure.BRIDGE_OCCUPIED }, null, 2)}\n`);
138
+ }
103
139
  process.exit(1);
104
140
  }
105
141
  return runSetupCommand({
106
142
  appDir: context.appDir,
107
143
  invokedAt: cwd,
108
144
  bridgePort: port,
145
+ // Read here because this is the layer that already owns the bridge: setup opens a lease over
146
+ // the daemon's MCP transport for `--no-open`, and that transport is gated on this token.
147
+ pairingToken: readOrCreatePairingTokenSync(defaultPairingTokenDir()),
109
148
  env: collectEnv(parsed.env ?? []),
110
149
  openBrowser: false !== parsed.open,
111
150
  registerAgents: wantsAgents(parsed),
@@ -116,11 +155,46 @@ export async function continueAfterInit(parsed, result, io, cwd) {
116
155
  // keeps deciding, so nobody who passed nothing waits less than they used to.
117
156
  ...(undefined === parsed.timeoutSeconds
118
157
  ? {}
119
- : { connectBudgetMs: parsed.timeoutSeconds * 1000 }),
158
+ : {
159
+ connectBudgetMs: parsed.timeoutSeconds * 1000,
160
+ startupBudgetMs: parsed.timeoutSeconds * 1000,
161
+ }),
120
162
  pollMs: POLL_MS,
121
- ...(undefined === context.devCommand ? {} : { devCommand: context.devCommand }),
163
+ htmlCarriesSdk: HTML_CARRIES_SDK.has(context.framework),
164
+ // A plain page with no dev script gets Reticle's own static server, so the page is actually
165
+ // served and the connect can be proved, instead of stopping at "start the app yourself".
166
+ ...(() => {
167
+ const devCommand = context.devCommand ??
168
+ (undefined === parsed.url
169
+ ? staticPageDevCommand({
170
+ appDir: context.appDir,
171
+ isStaticPage: Framework.HTML === context.framework,
172
+ })
173
+ : undefined);
174
+ return undefined === devCommand ? {} : { devCommand };
175
+ })(),
122
176
  ...(undefined === parsed.url ? {} : { suppliedUrl: parsed.url }),
123
177
  }, (line) => io.print(line)).then((outcome) => {
178
+ if (result.outcome !== undefined) {
179
+ const init = {
180
+ ...result.outcome,
181
+ ok: outcome.ok,
182
+ };
183
+ if (outcome.ok) {
184
+ delete init.reason;
185
+ init.confirmation = InitConfirmation.CONNECTED;
186
+ }
187
+ else {
188
+ // A failed startup does not establish whether a daemon or instrumented page exists.
189
+ // Keep the watcher's no_daemon/no_session/no_page classifications for observed facts.
190
+ delete init.confirmation;
191
+ init.reason =
192
+ outcome.reachedPhase === SetupPhase.DEV_SERVER
193
+ ? InitFailure.DEV_SERVER
194
+ : InitFailure.APP_CONNECTION;
195
+ }
196
+ reportInitOutcome(init);
197
+ }
124
198
  // One object, so an agent reads a result instead of interpreting a report.
125
199
  if (true === parsed.json) {
126
200
  process.stdout.write(`${JSON.stringify(outcome, null, 2)}\n`);
@@ -145,9 +219,12 @@ export async function continueAfterInit(parsed, result, io, cwd) {
145
219
  // Success, and no flow. Saying "a flow was driven" here would replace a wrong exit code with
146
220
  // a wrong sentence, which is the worse of the two: the exit code is read by CI and the
147
221
  // sentence is read by a person deciding whether their app is verified. It is not.
148
- io.print(`✓ ${outcome.url ?? 'the app'} is instrumented and connected — but NOT verified.`);
222
+ // The connect already said "connected, nothing verified yet" and listed what to do next;
223
+ // repeating it here was the same sentence twice, three lines apart.
149
224
  for (const [i, step] of outcome.fallback.entries())
150
225
  io.print(` ${String(i + 1)}. ${step}`);
226
+ // Already spaced by the blank line above when nothing was listed.
227
+ ask(0 < outcome.fallback.length);
151
228
  return;
152
229
  }
153
230
  if (outcome.ok) {
@@ -156,6 +233,7 @@ export async function continueAfterInit(parsed, result, io, cwd) {
156
233
  // themselves, and it deserves a line of its own rather than a paragraph that gets skimmed.
157
234
  io.print(' Read the FINDINGS above before moving on: a flow can pass with a failed request or a ' +
158
235
  'console error behind it, and that is the app, not the check.');
236
+ ask();
159
237
  return;
160
238
  }
161
239
  // A run that produced no verdict did not succeed, and the exit code is the one place a caller
@@ -163,6 +241,10 @@ export async function continueAfterInit(parsed, result, io, cwd) {
163
241
  io.print('⚠ setup did not finish. To carry on from here:');
164
242
  for (const [i, step] of outcome.fallback.entries())
165
243
  io.print(` ${i + 1}. ${step}`);
244
+ ask();
166
245
  process.exit(1);
246
+ }, (error) => {
247
+ reportFailure(InitFailure.RUNTIME_ERROR);
248
+ throw error;
167
249
  });
168
250
  }
@@ -15,31 +15,42 @@ import type { CandidateSession } from './session-pick.js';
15
15
  import type { PageProbe } from './probe/page-probe.js';
16
16
  /** The dev server this process started, and the only one it will ever stop. */
17
17
  export declare class OwnedDevServer {
18
+ private readonly logDir;
18
19
  private child;
19
- private buffer;
20
- private lastOutputAt;
20
+ private log;
21
+ private cwd;
22
+ private startedAt;
21
23
  private handedOver;
24
+ constructor(logDir?: string);
25
+ /**
26
+ * Start it with its output going to a bounded log, through a supervisor that outlives `init`.
27
+ *
28
+ * Not a pipe into THIS process. A pipe's read end belonged to `init`; once it exited nobody held
29
+ * it, and the server's next write (a compile error, an HMR line, SvelteKit's first warning) was an
30
+ * EPIPE that killed it: "it connected, then the app went away". And not a bare file either, which
31
+ * has no ceiling: an upstream echo loop grew one to 2.9 GB in five minutes. The supervisor reads
32
+ * the server's pipes for as long as they are open and rotates the file past a cap — see
33
+ * dev-server-log-cap.ts. It is the process group leader, so `pid()` and `stop()` cover both.
34
+ */
22
35
  start(command: string, cwd: string, env: Readonly<Record<string, string>>): void;
36
+ /** Where the server's output goes, for the line that hands it over. */
37
+ logPath(): string | undefined;
23
38
  /** The process group leader, for a caller that has to clean up what it handed over. */
24
39
  pid(): number | undefined;
25
40
  output(): string;
26
41
  exited(): boolean;
42
+ /** Since the log last grew, which is since the server last printed anything. */
27
43
  quietForMs(): number;
28
44
  /** Ports anything in this server's process tree is listening on. */
29
45
  listeningPorts(): number[];
30
- /** Hand the running server to the user. After this, `stop()` does nothing. */
31
46
  /**
32
- * Leave the server running and stop holding it.
33
- *
34
- * `unref` on the child is NOT enough: its stdout and stderr are pipes owned by this process, and
35
- * an open pipe keeps the event loop alive on its own. So `init` printed "setup complete" and then
36
- * hung until something killed it, turning a successful run into a non-zero exit — which is
37
- * exactly what the install gate reported, on a run whose every other assertion passed.
47
+ * Leave the server running and stop holding it. After this, `stop()` does nothing.
38
48
  *
39
- * The streams are unref'd rather than destroyed: the server is still writing to them, and
40
- * destroying the read end of a live pipe risks an EPIPE in somebody else's dev server.
49
+ * Only `unref` is needed: the output is the supervisor's, so no pipe of ours holds the event loop
50
+ * (`init` once printed "setup complete" and hung on one). `url` is recorded beside the log, so the
51
+ * next `init` attaches to this server rather than starting a second one beside it.
41
52
  */
42
- handOver(): void;
53
+ handOver(url?: string): void;
43
54
  stop(): void;
44
55
  }
45
56
  /**
@@ -50,9 +61,11 @@ export declare class OwnedDevServer {
50
61
  * server that is already running.
51
62
  *
52
63
  * On loopback, the announced host is tried first and the other loopback families follow when it
53
- * misses — so an IPv6-only Vite that printed `127.0.0.1` still counts as served (#884).
64
+ * is REFUSED — so an IPv6-only Vite that printed `127.0.0.1` still counts as served (#884). Only
65
+ * refused: a timeout means the right server is slow, and the caller polls again on the same host.
66
+ * See `isAddressMiss` for the Next 16 first run that hopping on a timeout broke.
54
67
  */
55
- export declare function probePage(url: string): Promise<PageProbe>;
68
+ export declare function probePage(url: string, timeoutMs?: number): Promise<PageProbe>;
56
69
  /** Sessions the daemon is holding, in the shape the picker reads. */
57
70
  export declare function listSessions(port: number): Promise<CandidateSession[]>;
58
71
  /** Whether this project has a saved flow, wherever the project keeps one. */
@@ -12,60 +12,101 @@
12
12
  * listening indefinitely.
13
13
  */
14
14
  import { spawn, spawnSync } from 'node:child_process';
15
- import { Socket } from 'node:net';
16
- import { existsSync, readdirSync } from 'node:fs';
15
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs';
17
16
  import { join } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { isAlive } from '../daemon/daemon.js';
19
+ import { DEV_SERVER_LOG_CAP_BYTES, ROTATED_SUFFIX } from './bringup/dev-server-log-cap.js';
20
+ import { devServerLogName, recordHandOver, sweepDeadDevServers, } from './bringup/dev-server-files.js';
18
21
  import { fetchStatus } from '../daemon/binding/daemon-status-probe.js';
22
+ import { reticleStateHome } from '../daemon/daemon.js';
19
23
  import { descendants, parseLsofPorts, parseNetstatListeners, parseWmicProcesses, } from './terminal/listeners.js';
20
- import { loopbackProbeUrls } from './probe/loopback-probe-urls.js';
24
+ import { isAddressMiss, loopbackProbeUrls } from './probe/loopback-probe-urls.js';
21
25
  const WINDOWS = 'win32' === process.platform;
26
+ /** Astro otherwise daemonizes automatically when it detects an agent terminal. */
27
+ const ASTRO_DEV_BACKGROUND_ENV = 'ASTRO_DEV_BACKGROUND';
22
28
  /** A page fetch that is slow is a page fetch that failed, for our purposes. */
23
29
  const PROBE_TIMEOUT_MS = 5_000;
24
- /**
25
- * Stop an inherited stdio pipe from holding the event loop open.
26
- *
27
- * A child's piped stdout is a Socket at runtime and a Readable in the types, which has no `unref`.
28
- * Narrowing through Socket keeps that honest rather than asserting it.
29
- */
30
- function releasePipe(stream) {
31
- if (stream instanceof Socket)
32
- stream.unref();
33
- }
30
+ /** The supervisor that owns the server's output and keeps its log bounded. */
31
+ const LOG_CAP_SCRIPT = fileURLToPath(new URL('./bringup/dev-server-log-cap.js', import.meta.url));
34
32
  /** The dev server this process started, and the only one it will ever stop. */
35
33
  export class OwnedDevServer {
34
+ logDir;
36
35
  child;
37
- buffer = '';
38
- lastOutputAt = Date.now();
36
+ log;
37
+ cwd;
38
+ startedAt = Date.now();
39
39
  handedOver = false;
40
+ constructor(logDir = reticleStateHome()) {
41
+ this.logDir = logDir;
42
+ }
43
+ /**
44
+ * Start it with its output going to a bounded log, through a supervisor that outlives `init`.
45
+ *
46
+ * Not a pipe into THIS process. A pipe's read end belonged to `init`; once it exited nobody held
47
+ * it, and the server's next write (a compile error, an HMR line, SvelteKit's first warning) was an
48
+ * EPIPE that killed it: "it connected, then the app went away". And not a bare file either, which
49
+ * has no ceiling: an upstream echo loop grew one to 2.9 GB in five minutes. The supervisor reads
50
+ * the server's pipes for as long as they are open and rotates the file past a cap — see
51
+ * dev-server-log-cap.ts. It is the process group leader, so `pid()` and `stop()` cover both.
52
+ */
40
53
  start(command, cwd, env) {
41
- const child = spawn(command, {
54
+ mkdirSync(this.logDir, { recursive: true });
55
+ // Logs of servers that are gone, from every project, so the state home stops growing a file
56
+ // per app ever started. This one's are replaced below anyway.
57
+ sweepDeadDevServers(this.logDir, { alive: isAlive, now: Date.now(), keep: cwd });
58
+ const log = join(this.logDir, devServerLogName(cwd));
59
+ // Unlinked rather than truncated: a previous server for this project may still hold the old
60
+ // file open, and truncating under it would interleave its lines into this run's output.
61
+ rmSync(log, { force: true });
62
+ rmSync(`${log}${ROTATED_SUFFIX}`, { force: true });
63
+ this.child = spawn(process.execPath, [LOG_CAP_SCRIPT, log, String(DEV_SERVER_LOG_CAP_BYTES), command], {
42
64
  cwd,
43
- shell: true,
44
65
  // Its own process group, so stopping it stops what it started rather than only the wrapper.
45
66
  detached: !WINDOWS,
46
- stdio: ['ignore', 'pipe', 'pipe'],
47
- env: { ...process.env, ...env },
67
+ // Nothing of ours: the supervisor holds the server's pipes, and init holds none of its.
68
+ stdio: 'ignore',
69
+ // Reticle already provides the detached supervisor. Astro's agent auto-backgrounding
70
+ // would escape its process group and leave the recorded handover PID pointing at a dead
71
+ // launcher, so re-init and cleanup could no longer find or stop the actual server.
72
+ env: { ...process.env, ...env, [ASTRO_DEV_BACKGROUND_ENV]: '1' },
48
73
  });
49
- const collect = (chunk) => {
50
- this.buffer += String(chunk);
51
- this.lastOutputAt = Date.now();
52
- };
53
- child.stdout?.on('data', collect);
54
- child.stderr?.on('data', collect);
55
- this.child = child;
74
+ this.log = log;
75
+ this.cwd = cwd;
76
+ this.startedAt = Date.now();
77
+ }
78
+ /** Where the server's output goes, for the line that hands it over. */
79
+ logPath() {
80
+ return this.log;
56
81
  }
57
82
  /** The process group leader, for a caller that has to clean up what it handed over. */
58
83
  pid() {
59
84
  return this.child?.pid;
60
85
  }
61
86
  output() {
62
- return this.buffer;
87
+ if (undefined === this.log)
88
+ return '';
89
+ try {
90
+ return readFileSync(this.log, 'utf8');
91
+ }
92
+ catch {
93
+ return '';
94
+ }
63
95
  }
64
96
  exited() {
65
97
  return undefined !== this.child && null !== this.child.exitCode;
66
98
  }
99
+ /** Since the log last grew, which is since the server last printed anything. */
67
100
  quietForMs() {
68
- return Date.now() - this.lastOutputAt;
101
+ let lastOutputAt = this.startedAt;
102
+ try {
103
+ if (undefined !== this.log)
104
+ lastOutputAt = Math.max(lastOutputAt, statSync(this.log).mtimeMs);
105
+ }
106
+ catch {
107
+ /* not written yet */
108
+ }
109
+ return Date.now() - lastOutputAt;
69
110
  }
70
111
  /** Ports anything in this server's process tree is listening on. */
71
112
  listeningPorts() {
@@ -90,24 +131,20 @@ export class OwnedDevServer {
90
131
  `lsof -a -p ${pids.join(',')} -iTCP -sTCP:LISTEN -P -n 2>/dev/null | awk 'NR>1{print $9}'`,
91
132
  ]));
92
133
  }
93
- /** Hand the running server to the user. After this, `stop()` does nothing. */
94
134
  /**
95
- * Leave the server running and stop holding it.
96
- *
97
- * `unref` on the child is NOT enough: its stdout and stderr are pipes owned by this process, and
98
- * an open pipe keeps the event loop alive on its own. So `init` printed "setup complete" and then
99
- * hung until something killed it, turning a successful run into a non-zero exit — which is
100
- * exactly what the install gate reported, on a run whose every other assertion passed.
135
+ * Leave the server running and stop holding it. After this, `stop()` does nothing.
101
136
  *
102
- * The streams are unref'd rather than destroyed: the server is still writing to them, and
103
- * destroying the read end of a live pipe risks an EPIPE in somebody else's dev server.
137
+ * Only `unref` is needed: the output is the supervisor's, so no pipe of ours holds the event loop
138
+ * (`init` once printed "setup complete" and hung on one). `url` is recorded beside the log, so the
139
+ * next `init` attaches to this server rather than starting a second one beside it.
104
140
  */
105
- handOver() {
141
+ handOver(url) {
106
142
  this.handedOver = true;
107
- const child = this.child;
108
- releasePipe(child?.stdout);
109
- releasePipe(child?.stderr);
110
- child?.unref();
143
+ this.child?.unref();
144
+ const pid = this.child?.pid;
145
+ if (undefined !== url && undefined !== pid && undefined !== this.cwd) {
146
+ recordHandOver(this.logDir, { pid, url, appDir: this.cwd });
147
+ }
111
148
  }
112
149
  stop() {
113
150
  const child = this.child;
@@ -164,14 +201,16 @@ function windowsProcessPairs() {
164
201
  * server that is already running.
165
202
  *
166
203
  * On loopback, the announced host is tried first and the other loopback families follow when it
167
- * misses — so an IPv6-only Vite that printed `127.0.0.1` still counts as served (#884).
204
+ * is REFUSED — so an IPv6-only Vite that printed `127.0.0.1` still counts as served (#884). Only
205
+ * refused: a timeout means the right server is slow, and the caller polls again on the same host.
206
+ * See `isAddressMiss` for the Next 16 first run that hopping on a timeout broke.
168
207
  */
169
- export async function probePage(url) {
208
+ export async function probePage(url, timeoutMs = PROBE_TIMEOUT_MS) {
170
209
  const candidates = loopbackProbeUrls(url);
171
210
  let sawTlsRefused = false;
172
211
  for (const candidate of candidates) {
173
212
  try {
174
- const res = await fetch(candidate, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
213
+ const res = await fetch(candidate, { signal: AbortSignal.timeout(timeoutMs) });
175
214
  const body = await res.text();
176
215
  const sdkInPage = /@reticlehq|@reticle-connect|reticle-dev/.test(body);
177
216
  // Only stamp reachedUrl when we had to leave the announcement — callers that already have
@@ -187,6 +226,8 @@ export async function probePage(url) {
187
226
  if (/certificate|SELF_SIGNED|DEPTH_ZERO|ERR_TLS|unable to verify/i.test(message)) {
188
227
  sawTlsRefused = true;
189
228
  }
229
+ if (!isAddressMiss(err))
230
+ break;
190
231
  }
191
232
  }
192
233
  return { served: false, sdkInPage: false, ...(sawTlsRefused ? { tlsRefused: true } : {}) };
@@ -16,3 +16,10 @@
16
16
  * family-split problem, and inventing alternates would probe the wrong machine.
17
17
  */
18
18
  export declare function loopbackProbeUrls(url: string): string[];
19
+ /**
20
+ * Whether a failed fetch was refused at the connection level on this address.
21
+ *
22
+ * `fetch` wraps the socket error as `cause`; with happy-eyeballs that cause is an AggregateError of
23
+ * one error per address tried, and it is a miss only when every one of them was.
24
+ */
25
+ export declare function isAddressMiss(err: unknown): boolean;
@@ -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;
@@ -114,9 +132,18 @@ export interface SetupInput {
114
132
  * before setup could say what was wrong.
115
133
  */
116
134
  readonly connectBudgetMs?: number | undefined;
135
+ /** Explicit --timeout for startup; absent preserves the adaptive build wait. */
136
+ readonly startupBudgetMs?: number | undefined;
117
137
  readonly openBrowser: boolean;
118
138
  /** Web, Electron or Tauri. Desktop changes three things; see desktop-shape.ts. */
119
139
  readonly shape: AppShape;
140
+ /**
141
+ * The served HTML carries the SDK marker on this shape: Vite's index.html injection, or a plain
142
+ * HTML page with the snippet pasted in. Every other framework delivers the connect in the JS
143
+ * bundle, so "the SDK is NOT in the page" is the permanent state of a healthy page there, and
144
+ * saying it sent people to restart a dev server that was fine.
145
+ */
146
+ readonly htmlCarriesSdk: boolean;
120
147
  readonly phaseTimeoutMs: number;
121
148
  readonly pollMs: number;
122
149
  }