@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
@@ -8,51 +8,170 @@
8
8
  * dial and setup ends with "the SDK is in the page and never dialled the bridge" — the SDK being
9
9
  * blamed for the absence of the thing it was dialling.
10
10
  *
11
- * Idempotent by construction: an alive daemon for this project is adopted, and one belonging to
12
- * somebody else is refused rather than stolen, which is the same rule `reticle mcp` follows.
11
+ * Idempotent by construction: a daemon that speaks our wire contract is adopted. One that does NOT
12
+ * is not usable, however healthy its `/status`: an older Reticle turns away every hello from the page
13
+ * init just instrumented ("bridge refused the connection: invalid message"), and init used to adopt
14
+ * it anyway, wait out its budget, and blame the page. So a skewed daemon nobody is using is replaced
15
+ * (stopped by its LISTENER, never `lsof -ti`, which also kills the agent's MCP proxy), and one with
16
+ * live sessions is left alone with the exact command to run once they are done.
17
+ *
18
+ * A stranger on the port that is not a Reticle daemon at all is refused before this runs; see
19
+ * `bridge-port.ts`.
13
20
  */
14
21
  import { log } from '../../../log.js';
15
- import { spawnDaemon } from '../../daemon/daemon.js';
22
+ import { isAlive, readPid, reticleStateHome, spawnDaemon } from '../../daemon/daemon.js';
23
+ import { readDaemonRegistry } from '../../daemon/daemon-resolve.js';
24
+ import { LOOPBACK_HOST, STATUS_HOLD_MS } from '@reticlehq/core';
16
25
  import { probePresence, presenceIsUsable } from '../../daemon/binding/port-presence.js';
17
- import { waitForDaemon, probeDaemon } from '../../../surface/mcp/mcp-proxy.js';
26
+ import { probeDaemon, waitForDaemonBind } from '../../../surface/mcp/mcp-proxy.js';
18
27
  import { daemonSpawnArgs } from '../../cli/daemon-start-options.js';
19
- import { fetchStatus } from '../../daemon/binding/daemon-status-probe.js';
28
+ import { fetchStatus, summarizeStatus } from '../../daemon/binding/daemon-status-probe.js';
29
+ import { runKill } from '../../cli/cli-kill.js';
20
30
  export const EnsureDaemon = {
21
31
  /** Something usable was already there. */
22
32
  ADOPTED: 'adopted',
23
33
  STARTED: 'started',
34
+ /** A daemon on another wire contract, serving nobody, was stopped and ours started in its place. */
35
+ REPLACED: 'replaced',
36
+ /** A daemon on another wire contract is serving live sessions, so it was left alone. */
37
+ SKEWED_IN_USE: 'skewed-in-use',
24
38
  /** Could not be started, and the caller has to say so rather than blame the page. */
25
39
  UNAVAILABLE: 'unavailable',
26
40
  };
27
- export async function ensureDaemon(port, deps) {
28
- if (await deps.usable(port))
29
- return EnsureDaemon.ADOPTED;
41
+ /** A string field off the /status body, or undefined on a daemon too old to report it. */
42
+ function statusString(payload, key) {
43
+ if ('object' !== typeof payload || null === payload)
44
+ return undefined;
45
+ const value = Reflect.get(payload, key);
46
+ return 'string' === typeof value && 0 < value.length ? value : undefined;
47
+ }
48
+ async function spawnAndWait(port, deps) {
30
49
  if (undefined === deps.scriptPath)
31
- return EnsureDaemon.UNAVAILABLE;
32
- if (!deps.spawn(port))
33
- return EnsureDaemon.UNAVAILABLE;
50
+ return false;
51
+ // A lost spawn race is not a failed start: `init` printed "could not start the Reticle daemon"
52
+ // over a daemon that bound seconds later. Only a spawn that failed with nobody else starting one
53
+ // is a reason to stop without waiting.
54
+ if (!deps.spawn(port) && !deps.starting(port))
55
+ return false;
34
56
  try {
35
57
  // Readiness means the port ACCEPTS, not that a child was spawned: a readiness signal that
36
58
  // precedes readiness is worse than none, because what waits on it stops waiting too early.
37
59
  await deps.waitReady(port);
38
60
  }
39
61
  catch {
40
- return EnsureDaemon.UNAVAILABLE;
62
+ return false;
63
+ }
64
+ return deps.usable(port);
65
+ }
66
+ export async function ensureDaemon(port, deps) {
67
+ if (await deps.usable(port)) {
68
+ const status = await deps.status(port);
69
+ if (!deps.skewed(status))
70
+ return { state: EnsureDaemon.ADOPTED };
71
+ const theirs = statusString(status, 'version') ?? 'an older build';
72
+ const { sessionCount } = summarizeStatus(status);
73
+ if (0 < sessionCount) {
74
+ return {
75
+ state: EnsureDaemon.SKEWED_IN_USE,
76
+ message: `The Reticle daemon on port ${String(port)} is ${theirs}, which speaks a different wire ` +
77
+ `contract than this init, so it will refuse your app — and ` +
78
+ `${String(sessionCount)} session(s) are still using it, so init will not stop it. When ` +
79
+ `they are done, run \`npx @reticlehq/server kill --port ${String(port)}\` and re-run init.`,
80
+ };
81
+ }
82
+ if (!(await deps.replace(port)))
83
+ return { state: EnsureDaemon.UNAVAILABLE };
84
+ return (await spawnAndWait(port, deps))
85
+ ? {
86
+ state: EnsureDaemon.REPLACED,
87
+ message: `replaced the idle Reticle daemon on port ${String(port)} (${theirs}), which speaks a ` +
88
+ `different wire contract and would have refused your app, with a current one.`,
89
+ }
90
+ : { state: EnsureDaemon.UNAVAILABLE };
91
+ }
92
+ return (await spawnAndWait(port, deps))
93
+ ? { state: EnsureDaemon.STARTED }
94
+ : { state: EnsureDaemon.UNAVAILABLE };
95
+ }
96
+ /**
97
+ * Stop this project's daemons on any port but `port`, the one init is about to use. Returns the
98
+ * ports stopped.
99
+ *
100
+ * `init --port <new>` started a daemon on the new port and left the old one serving: the build
101
+ * plugin found both, dialled the old one, and init — waiting on the new one — exited 1 with
102
+ * "connected to a DIFFERENT Reticle daemon". A daemon another project's tab is still on is left
103
+ * alone, because stopping it would take that app down to fix this one. Stopped by its listener
104
+ * (`runKill` without force), never `lsof -ti`, which also kills the agent's MCP proxy.
105
+ */
106
+ export async function retireMovedDaemons(projectId, port, deps) {
107
+ if (undefined === projectId || 0 === projectId.length)
108
+ return [];
109
+ const stopped = [];
110
+ for (const entry of deps.registered()) {
111
+ if (entry.projectId !== projectId || entry.port === port)
112
+ continue;
113
+ const { sessions } = summarizeStatus(await deps.status(entry.port));
114
+ const shared = sessions.some((s) => undefined !== s.projectId && projectId !== s.projectId);
115
+ if (shared)
116
+ continue;
117
+ if (await deps.stop(entry.port))
118
+ stopped.push(entry.port);
41
119
  }
42
- return (await deps.usable(port)) ? EnsureDaemon.STARTED : EnsureDaemon.UNAVAILABLE;
120
+ return stopped;
121
+ }
122
+ /** The real registry, `/status` and listener-only stop. */
123
+ export function nodeRetireDeps() {
124
+ return {
125
+ registered: () => readDaemonRegistry(reticleStateHome()).filter((e) => isAlive(e.pid)),
126
+ status: (port) => fetchStatus(port),
127
+ stop: (port) => runKill(port, false),
128
+ };
129
+ }
130
+ /**
131
+ * Hold the daemon on `port` until the returned release is called, by renewing a hold on its
132
+ * `/status` well inside the hold window.
133
+ *
134
+ * `init` starts the daemon and can then wait on the app for longer than the idle grace — a cold
135
+ * desktop build, a slow first compile. None of that counted as activity, and the daemon init had
136
+ * just started idled out (`reticle_daemon_idle_exit`) while init was still waiting on it.
137
+ */
138
+ export function holdDaemon(port) {
139
+ const renew = () => void fetchStatus(port, LOOPBACK_HOST, true);
140
+ renew();
141
+ const timer = setInterval(renew, STATUS_HOLD_MS / 3);
142
+ timer.unref();
143
+ return () => clearInterval(timer);
43
144
  }
44
- export function nodeEnsureDaemonDeps() {
145
+ export function nodeEnsureDaemonDeps(skewed) {
45
146
  return {
147
+ skewed,
46
148
  usable: async (port) => presenceIsUsable(await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus })),
149
+ status: (port) => fetchStatus(port),
150
+ // `reticle kill` without `--force`: it signals only the LISTENER, and only one that answers
151
+ // /status, so the agent's MCP proxy on the same port survives.
152
+ replace: (port) => runKill(port, false),
47
153
  spawn: (port) => {
48
154
  const scriptPath = process.argv[1];
49
155
  if (undefined === scriptPath)
50
156
  return false;
51
157
  const started = spawnDaemon(process.execPath, scriptPath, daemonSpawnArgs({ port, headless: true, http: false }), port);
52
- log('reticle_setup_daemon_started', { port, started });
158
+ // For a log or a pipe. On a terminal it was a JSON line in the middle of `init`'s output, read
159
+ // by a person, about a step the next lines already report in words.
160
+ if (true !== process.stderr.isTTY)
161
+ log('reticle_setup_daemon_started', { port, started });
53
162
  return started;
54
163
  },
55
- waitReady: (port) => waitForDaemon(port),
164
+ starting: (port) => {
165
+ const pid = readPid(port);
166
+ return null !== pid && isAlive(pid);
167
+ },
168
+ // The same wait `reticle restart` does: poll until `/status` answers as a daemon, bounded. A
169
+ // cold daemon on a fresh HOME outlived the proxy's shorter reconnect budget this used to borrow.
170
+ waitReady: async (port) => {
171
+ if (!(await waitForDaemonBind(port)))
172
+ throw new Error(`no daemon bound :${String(port)}`);
173
+ return undefined;
174
+ },
56
175
  scriptPath: process.argv[1],
57
176
  };
58
177
  }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The dev server `init` starts for a plain HTML page that has none of its own.
3
+ *
4
+ * A directory holding only `index.html` (and a script or two) used to be wired correctly — init
5
+ * writes the connect snippet into the page — and then stop at "No dev command … start the app
6
+ * yourself and pass --url". The one thing left between that user and a connected session was a web
7
+ * server, which is the one thing a page like that never had. So init starts this one, the same way it
8
+ * starts `npm run dev`, and the rest of setup (wait, open, prove the connect) runs unchanged.
9
+ *
10
+ * It is run as its own process (`node static-page-server.js --dir <app>`), not inside init, because it
11
+ * has to outlive init exactly like any handed-over dev server. It imports nothing but Node, so the
12
+ * built file runs on its own.
13
+ *
14
+ * Loopback only, on both families: a page served to the LAN would publish the pairing token the
15
+ * snippet carries.
16
+ */
17
+ export declare const STATIC_PAGE_INDEX = "index.html";
18
+ /** Where the search for a free port starts when none is given — clear of Vite's 5173 run. */
19
+ export declare const STATIC_PAGE_FIRST_PORT = 5500;
20
+ export interface StaticPageServer {
21
+ readonly url: string;
22
+ close(): Promise<void>;
23
+ }
24
+ export declare function startStaticPageServer(opts: {
25
+ root: string;
26
+ port: number;
27
+ }): Promise<StaticPageServer>;
28
+ /**
29
+ * The command setup runs for a page with no dev server of its own, or undefined when this is not
30
+ * that page. Quoted so an app path with a space survives the shell.
31
+ */
32
+ export declare function staticPageDevCommand(opts: {
33
+ appDir: string;
34
+ isStaticPage: boolean;
35
+ }): string | undefined;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * The dev server `init` starts for a plain HTML page that has none of its own.
3
+ *
4
+ * A directory holding only `index.html` (and a script or two) used to be wired correctly — init
5
+ * writes the connect snippet into the page — and then stop at "No dev command … start the app
6
+ * yourself and pass --url". The one thing left between that user and a connected session was a web
7
+ * server, which is the one thing a page like that never had. So init starts this one, the same way it
8
+ * starts `npm run dev`, and the rest of setup (wait, open, prove the connect) runs unchanged.
9
+ *
10
+ * It is run as its own process (`node static-page-server.js --dir <app>`), not inside init, because it
11
+ * has to outlive init exactly like any handed-over dev server. It imports nothing but Node, so the
12
+ * built file runs on its own.
13
+ *
14
+ * Loopback only, on both families: a page served to the LAN would publish the pairing token the
15
+ * snippet carries.
16
+ */
17
+ import { existsSync, readFile, statSync } from 'node:fs';
18
+ import { createServer } from 'node:http';
19
+ import { extname, join, resolve, sep } from 'node:path';
20
+ import { fileURLToPath, pathToFileURL } from 'node:url';
21
+ export const STATIC_PAGE_INDEX = 'index.html';
22
+ /** Where the search for a free port starts when none is given — clear of Vite's 5173 run. */
23
+ export const STATIC_PAGE_FIRST_PORT = 5500;
24
+ const PORT_SEARCH_SPAN = 50;
25
+ const DIR_FLAG = '--dir';
26
+ const PORT_FLAG = '--port';
27
+ const LOOPBACK_V4 = '127.0.0.1';
28
+ const LOOPBACK_V6 = '::1';
29
+ const ANNOUNCED_HOST = 'localhost';
30
+ const CONTENT_TYPES = {
31
+ '.html': 'text/html; charset=utf-8',
32
+ '.js': 'text/javascript; charset=utf-8',
33
+ '.mjs': 'text/javascript; charset=utf-8',
34
+ '.css': 'text/css; charset=utf-8',
35
+ '.json': 'application/json; charset=utf-8',
36
+ '.svg': 'image/svg+xml',
37
+ '.png': 'image/png',
38
+ '.jpg': 'image/jpeg',
39
+ '.jpeg': 'image/jpeg',
40
+ '.gif': 'image/gif',
41
+ '.webp': 'image/webp',
42
+ '.ico': 'image/x-icon',
43
+ '.woff2': 'font/woff2',
44
+ '.txt': 'text/plain; charset=utf-8',
45
+ };
46
+ const FALLBACK_TYPE = 'application/octet-stream';
47
+ /** The file a request names, or null when it is missing or outside `root`. */
48
+ function fileFor(root, rawUrl) {
49
+ let path;
50
+ try {
51
+ path = decodeURIComponent(new URL(rawUrl, 'http://x').pathname);
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ const target = resolve(root, `.${path}`);
57
+ if (target !== root && !target.startsWith(`${root}${sep}`))
58
+ return null;
59
+ const file = path.endsWith('/') ? join(target, STATIC_PAGE_INDEX) : target;
60
+ try {
61
+ return statSync(file).isFile() ? file : null;
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ function listen(server, port, host) {
68
+ return new Promise((ok, fail) => {
69
+ server.once('error', fail);
70
+ server.listen(port, host, () => {
71
+ server.off('error', fail);
72
+ ok();
73
+ });
74
+ });
75
+ }
76
+ function closeServer(server) {
77
+ return new Promise((done) => server.close(() => done()));
78
+ }
79
+ export async function startStaticPageServer(opts) {
80
+ const root = resolve(opts.root);
81
+ const handler = (req, res) => {
82
+ const file = fileFor(root, req.url ?? '/');
83
+ if (null === file) {
84
+ res.writeHead(404).end();
85
+ return;
86
+ }
87
+ readFile(file, (err, body) => {
88
+ if (null !== err) {
89
+ res.writeHead(404).end();
90
+ return;
91
+ }
92
+ res.writeHead(200, {
93
+ 'content-type': CONTENT_TYPES[extname(file).toLowerCase()] ?? FALLBACK_TYPE,
94
+ 'cache-control': 'no-store',
95
+ });
96
+ res.end(body);
97
+ });
98
+ };
99
+ const v4 = createServer(handler);
100
+ // Port 0 means "any": the v4 bind picks it, and the v6 listener joins the same number.
101
+ const candidates = 0 === opts.port ? [0] : Array.from({ length: PORT_SEARCH_SPAN }, (_, i) => opts.port + i);
102
+ let bound;
103
+ for (const port of candidates) {
104
+ try {
105
+ await listen(v4, port, LOOPBACK_V4);
106
+ bound = v4.address().port;
107
+ break;
108
+ }
109
+ catch {
110
+ /* taken: try the next one */
111
+ }
112
+ }
113
+ if (undefined === bound) {
114
+ throw new Error(`no free port in ${String(opts.port)}–${String(opts.port + PORT_SEARCH_SPAN)}`);
115
+ }
116
+ // `localhost` resolves to ::1 first on macOS, so the page must answer there too. A machine with
117
+ // no IPv6 loopback just keeps the v4 listener.
118
+ const v6 = createServer(handler);
119
+ const hasV6 = await listen(v6, bound, LOOPBACK_V6).then(() => true, () => false);
120
+ return {
121
+ url: `http://${ANNOUNCED_HOST}:${String(bound)}/`,
122
+ close: async () => {
123
+ await closeServer(v4);
124
+ if (hasV6)
125
+ await closeServer(v6);
126
+ },
127
+ };
128
+ }
129
+ /**
130
+ * The command setup runs for a page with no dev server of its own, or undefined when this is not
131
+ * that page. Quoted so an app path with a space survives the shell.
132
+ */
133
+ export function staticPageDevCommand(opts) {
134
+ if (!opts.isStaticPage || !existsSync(join(opts.appDir, STATIC_PAGE_INDEX)))
135
+ return undefined;
136
+ const script = fileURLToPath(import.meta.url).replace(/\.ts$/, '.js');
137
+ return `"${process.execPath}" "${script}" ${DIR_FLAG} "${opts.appDir}"`;
138
+ }
139
+ function flag(argv, name) {
140
+ const at = argv.indexOf(name);
141
+ return -1 === at ? undefined : argv[at + 1];
142
+ }
143
+ // Run as a script: serve until killed, printing the url the setup wait reads.
144
+ if (undefined !== process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
145
+ const dir = flag(process.argv, DIR_FLAG) ?? process.cwd();
146
+ const port = Number(flag(process.argv, PORT_FLAG) ?? STATIC_PAGE_FIRST_PORT);
147
+ startStaticPageServer({ root: dir, port }).then((server) => process.stdout.write(`serving ${dir}\n Local: ${server.url}\n`), (err) => {
148
+ process.stderr.write(`static page server: ${String(err)}\n`);
149
+ process.exit(1);
150
+ });
151
+ }
@@ -53,6 +53,10 @@ interface ShapePolicy {
53
53
  readonly connectBudgetMs: number;
54
54
  /** Said out loud, because a desktop run looks stuck if nobody explains the wait. */
55
55
  readonly note: string | undefined;
56
+ /** What the connect wait is waiting on, for the progress lines it prints while it waits. */
57
+ readonly awaiting: string;
56
58
  }
59
+ /** "a Tauri app", "an Electron app": printed when the shape is detected. */
60
+ export declare function describeShape(shape: AppShape): string;
57
61
  export declare function policyFor(shape: AppShape): ShapePolicy;
58
62
  export {};
@@ -42,7 +42,21 @@ export function isDesktopOrigin(url) {
42
42
  }
43
43
  const WEB_CONNECT_MS = 120_000;
44
44
  /** A cold `tauri dev` compiles the Rust side before a window exists. */
45
- const DESKTOP_CONNECT_MS = 10 * 60_000;
45
+ const TAURI_CONNECT_MS = 10 * 60_000;
46
+ /**
47
+ * Electron has no compile step of that size: its window is a Node process loading a dev server that
48
+ * is already up. It was given Tauri's ten minutes, so a run whose window was never going to dial in
49
+ * sat silent for all of them — longer than the daemon it was waiting on stayed alive.
50
+ */
51
+ const ELECTRON_CONNECT_MS = 3 * 60_000;
52
+ /** "a Tauri app", "an Electron app": printed when the shape is detected. */
53
+ export function describeShape(shape) {
54
+ if (AppShape.ELECTRON === shape)
55
+ return 'an Electron app';
56
+ if (AppShape.TAURI === shape)
57
+ return 'a Tauri app';
58
+ return 'a web app';
59
+ }
46
60
  export function policyFor(shape) {
47
61
  if (AppShape.WEB === shape) {
48
62
  return {
@@ -50,12 +64,14 @@ export function policyFor(shape) {
50
64
  requireHttpReady: true,
51
65
  connectBudgetMs: WEB_CONNECT_MS,
52
66
  note: undefined,
67
+ awaiting: 'the page',
53
68
  };
54
69
  }
55
70
  return {
56
71
  openBrowser: false,
57
72
  requireHttpReady: false,
58
- connectBudgetMs: DESKTOP_CONNECT_MS,
73
+ connectBudgetMs: AppShape.TAURI === shape ? TAURI_CONNECT_MS : ELECTRON_CONNECT_MS,
74
+ awaiting: AppShape.TAURI === shape ? 'the Tauri window' : 'the Electron window',
59
75
  note: AppShape.TAURI === shape
60
76
  ? 'Tauri app: not opening a browser, because its own window is the client. A cold `tauri dev` builds the Rust side first, so the window can take minutes to appear.'
61
77
  : 'Electron app: not opening a browser, because its own window is the client. Waiting for the app to launch and dial in.',
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `RETICLE_PORT`, when it names a port. The one reader of it for the CLI, `init` included.
3
+ *
4
+ * `init` took only `--port`, so a shell that exported RETICLE_PORT got a project wired, and a daemon
5
+ * started, on the default port while every other command it ran dialled the exported one. Fed to
6
+ * `portForInit` as the explicit port, so the files phase writes it into `.reticle.json` and the
7
+ * runtime phase binds it: the two halves of `init` cannot disagree.
8
+ */
9
+ export declare function portFromEnv(env: Readonly<Record<string, string | undefined>>): number | undefined;
10
+ export declare function portForInit(explicit: number | undefined, configured: number | undefined, projectId: string | undefined, deps: {
11
+ /** The projects the daemon on `port` belongs to or is serving right now. */
12
+ daemonProjects: (port: number) => Promise<readonly string[]>;
13
+ daemonPresent: (port: number) => Promise<boolean>;
14
+ pickPort: (preferred: number) => Promise<number>;
15
+ }): Promise<number | undefined>;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The daemon port `reticle init` wires a project to.
3
+ *
4
+ * Init used to leave this unset, so every project on a machine was wired to the default port. The
5
+ * second one to run was connected to the first one's daemon, and the bridge refused its app. When a
6
+ * daemon for another project already holds the default, the new project gets its own port, and init
7
+ * records it in `.reticle.json`, where the CLI and the build plugins both read it.
8
+ *
9
+ * Undefined means the default.
10
+ */
11
+ import { RETICLE_DEFAULT_PORT, ReticleEnv } from '@reticlehq/core';
12
+ const MAX_PORT = 65_535;
13
+ /**
14
+ * `RETICLE_PORT`, when it names a port. The one reader of it for the CLI, `init` included.
15
+ *
16
+ * `init` took only `--port`, so a shell that exported RETICLE_PORT got a project wired, and a daemon
17
+ * started, on the default port while every other command it ran dialled the exported one. Fed to
18
+ * `portForInit` as the explicit port, so the files phase writes it into `.reticle.json` and the
19
+ * runtime phase binds it: the two halves of `init` cannot disagree.
20
+ */
21
+ export function portFromEnv(env) {
22
+ const raw = env[ReticleEnv.PORT];
23
+ if (undefined === raw || !/^\d+$/.test(raw))
24
+ return undefined;
25
+ const port = Number(raw);
26
+ return 0 < port && port <= MAX_PORT ? port : undefined;
27
+ }
28
+ export async function portForInit(explicit, configured, projectId, deps) {
29
+ if (explicit !== undefined)
30
+ return explicit;
31
+ if (configured !== undefined)
32
+ return configured;
33
+ // Undefined, not the default's number: an explicit default would be written into every generated
34
+ // config as a `port` nobody chose.
35
+ if (!(await deps.daemonPresent(RETICLE_DEFAULT_PORT)))
36
+ return undefined;
37
+ // The bridge's own rule: it refuses a page only when it has served exactly ONE other project. A
38
+ // daemon serving nobody, several projects, or ours accepts this one. A project that has never
39
+ // been through init has no id yet, so a single named holder is somebody else.
40
+ const holders = await deps.daemonProjects(RETICLE_DEFAULT_PORT);
41
+ if (1 !== holders.length || holders[0] === projectId)
42
+ return undefined;
43
+ return deps.pickPort(RETICLE_DEFAULT_PORT + 1);
44
+ }
@@ -5,7 +5,7 @@
5
5
  * cli.ts is a dispatcher: a command that grew a second half should not make the file that routes
6
6
  * every command harder to read.
7
7
  */
8
- import type { InitResult } from '@reticlehq/init';
8
+ import { type InitResult } from '@reticlehq/init';
9
9
  /** Just enough of the parsed command to decide and run. */
10
10
  interface InitRuntimeArgs {
11
11
  readonly port: number | undefined;