@reticlehq/server 3.5.0 → 3.6.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 (328) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +16 -8
  3. package/dist/chromium-launch-options.d.ts +3 -1
  4. package/dist/chromium-launch-options.js +2 -1
  5. package/dist/command/cli/adhoc-verdict.d.ts +1 -0
  6. package/dist/command/cli/adhoc-verdict.js +6 -3
  7. package/dist/command/cli/answers/open-note.d.ts +2 -0
  8. package/dist/command/cli/answers/open-note.js +22 -0
  9. package/dist/command/cli/cli-flow-commands.js +4 -8
  10. package/dist/command/cli/cli-parse.d.ts +1 -1
  11. package/dist/command/cli/cli-parse.js +10 -10
  12. package/dist/command/cli/cli-verify.d.ts +1 -1
  13. package/dist/command/cli/cli-verify.js +35 -25
  14. package/dist/command/cli/cloud-cli.js +51 -12
  15. package/dist/command/cli/cloud-login.d.ts +8 -0
  16. package/dist/command/cli/cloud-login.js +36 -2
  17. package/dist/command/cli/demo/demo-page.js +11 -3
  18. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +46 -0
  19. package/dist/command/cli/doctor/browser/chromium-hint.js +99 -1
  20. package/dist/command/cli/drive/drive-attach.d.ts +27 -2
  21. package/dist/command/cli/drive/drive-attach.js +71 -4
  22. package/dist/command/cli/drive/drive-command.d.ts +2 -1
  23. package/dist/command/cli/drive/drive-command.js +13 -7
  24. package/dist/command/cli/lifecycle/daemon-lifecycle.js +4 -1
  25. package/dist/command/cli/ports/port-holder.d.ts +10 -0
  26. package/dist/command/cli/ports/port-holder.js +24 -0
  27. package/dist/command/cli/setup-mcp-cli.js +6 -4
  28. package/dist/command/cli/tutorial.d.ts +2 -2
  29. package/dist/command/cli/tutorial.js +6 -9
  30. package/dist/command/cli.d.ts +6 -0
  31. package/dist/command/cli.js +28 -22
  32. package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
  33. package/dist/command/daemon/binding/daemon-status-probe.js +1 -0
  34. package/dist/command/daemon/daemon-resolve.d.ts +14 -0
  35. package/dist/command/daemon/daemon-resolve.js +33 -2
  36. package/dist/command/setup/agent-writer.d.ts +1 -1
  37. package/dist/command/setup/agent-writer.js +3 -2
  38. package/dist/command/setup/bringup/connect-lease.d.ts +1 -0
  39. package/dist/command/setup/bringup/connect-lease.js +1 -0
  40. package/dist/command/setup/bringup/dev-server-wait.d.ts +8 -0
  41. package/dist/command/setup/bringup/dev-server-wait.js +19 -0
  42. package/dist/command/setup/init/init-port.d.ts +4 -0
  43. package/dist/command/setup/init/init-port.js +10 -0
  44. package/dist/command/setup/init/init-runtime.d.ts +10 -0
  45. package/dist/command/setup/init/init-runtime.js +18 -1
  46. package/dist/command/setup/node-effects.js +2 -1
  47. package/dist/command/setup/remaining-steps.js +2 -1
  48. package/dist/command/setup/run-setup.d.ts +5 -0
  49. package/dist/command/setup/run-setup.js +35 -27
  50. package/dist/command/setup/setup-command.d.ts +2 -1
  51. package/dist/command/setup/setup-command.js +2 -0
  52. package/dist/command/version/version-skew.js +11 -2
  53. package/dist/features/crawl/crawl-tools.js +4 -0
  54. package/dist/features/crawl/crawl.d.ts +6 -0
  55. package/dist/features/crawl/crawl.js +9 -1
  56. package/dist/features/exhaust/ledger.d.ts +7 -0
  57. package/dist/features/exhaust/ledger.js +35 -9
  58. package/dist/features/harness/app-findings.d.ts +2 -0
  59. package/dist/features/harness/app-findings.js +52 -0
  60. package/dist/features/harness/drive-report.d.ts +27 -1
  61. package/dist/features/harness/drive-report.js +162 -11
  62. package/dist/features/harness/drivers.d.ts +10 -14
  63. package/dist/features/harness/drivers.js +10 -14
  64. package/dist/features/harness/fill-values.d.ts +3 -73
  65. package/dist/features/harness/fill-values.js +3 -169
  66. package/dist/features/harness/goals.d.ts +21 -0
  67. package/dist/features/harness/goals.js +77 -0
  68. package/dist/features/harness/harness.d.ts +21 -0
  69. package/dist/features/harness/harness.js +69 -2
  70. package/dist/features/harness/platform/drive-target.d.ts +30 -0
  71. package/dist/features/harness/platform/drive-target.js +81 -0
  72. package/dist/features/harness/platform/local-apps.d.ts +94 -0
  73. package/dist/features/harness/platform/local-apps.js +204 -0
  74. package/dist/features/harness/platform/remote-drive.d.ts +180 -0
  75. package/dist/features/harness/platform/remote-drive.js +365 -0
  76. package/dist/features/harness/platform/script.d.ts +48 -0
  77. package/dist/features/harness/platform/script.js +128 -0
  78. package/dist/features/harness/platform/server-driver.d.ts +41 -0
  79. package/dist/features/harness/platform/server-driver.js +179 -0
  80. package/dist/features/harness/platform-config.d.ts +7 -0
  81. package/dist/features/harness/platform-config.js +14 -2
  82. package/dist/features/harness/script/app-controls.d.ts +5 -0
  83. package/dist/features/harness/script/app-controls.js +40 -0
  84. package/dist/features/harness/script/lane-ids.d.ts +14 -0
  85. package/dist/features/harness/script/lane-ids.js +16 -0
  86. package/dist/features/harness/script/script-run.d.ts +41 -0
  87. package/dist/features/harness/script/script-run.js +299 -0
  88. package/dist/features/license/license.d.ts +2 -2
  89. package/dist/features/visual/visual-tools.d.ts +10 -1
  90. package/dist/features/visual/visual-tools.js +40 -3
  91. package/dist/hooks/driven-by.d.ts +4 -0
  92. package/dist/hooks/driven-by.js +16 -0
  93. package/dist/hooks/hook-commands.js +15 -4
  94. package/dist/hooks/tool-log.d.ts +9 -0
  95. package/dist/hooks/tool-log.js +29 -0
  96. package/dist/index.d.ts +1 -0
  97. package/dist/index.js +76 -82
  98. package/dist/judgement/runs/artifact/build-verification-run.js +6 -0
  99. package/dist/judgement/runs/artifact/replay-mapping.d.ts +1 -1
  100. package/dist/judgement/runs/artifact/replay-mapping.js +16 -6
  101. package/dist/judgement/runs/artifact/run-store.d.ts +1 -1
  102. package/dist/judgement/runs/artifact/run-store.js +26 -1
  103. package/dist/judgement/runs/drive-run.d.ts +22 -0
  104. package/dist/judgement/runs/drive-run.js +99 -1
  105. package/dist/judgement/runs/repair-prompt.js +1 -1
  106. package/dist/judgement/runs/reticle-runner.js +8 -1
  107. package/dist/judgement/runs/verification-sync.d.ts +3 -2
  108. package/dist/judgement/runs/verification-sync.js +4 -3
  109. package/dist/language/flows/change/gate.d.ts +10 -0
  110. package/dist/language/flows/change/gate.js +22 -0
  111. package/dist/language/flows/decision.js +34 -0
  112. package/dist/language/flows/flow-anchor.d.ts +54 -8
  113. package/dist/language/flows/flow-anchor.js +88 -3
  114. package/dist/language/flows/flow-cross-step.d.ts +15 -0
  115. package/dist/language/flows/flow-cross-step.js +21 -0
  116. package/dist/language/flows/flow-learning.js +8 -11
  117. package/dist/language/flows/flow-mutate-tools.js +5 -3
  118. package/dist/language/flows/flow-replay-run.d.ts +23 -9
  119. package/dist/language/flows/flow-replay-run.js +101 -94
  120. package/dist/language/flows/flow-replay.d.ts +23 -1
  121. package/dist/language/flows/flow-replay.js +92 -19
  122. package/dist/language/flows/flow-scope.js +5 -1
  123. package/dist/language/flows/flow-step-runners.d.ts +20 -2
  124. package/dist/language/flows/flow-step-runners.js +236 -42
  125. package/dist/language/flows/flow-store-for-session.d.ts +2 -2
  126. package/dist/language/flows/flow-store-for-session.js +2 -2
  127. package/dist/language/flows/flow-tools.js +58 -34
  128. package/dist/language/flows/flows.js +5 -2
  129. package/dist/language/flows/heal-run.js +6 -4
  130. package/dist/language/flows/recording/flake.d.ts +2 -2
  131. package/dist/language/flows/recording/tape/recordings.d.ts +30 -4
  132. package/dist/language/flows/recording/tape/recordings.js +40 -8
  133. package/dist/language/flows/replay-from-hud.d.ts +20 -0
  134. package/dist/language/flows/replay-from-hud.js +51 -0
  135. package/dist/language/flows/replay.d.ts +9 -5
  136. package/dist/language/flows/replay.js +27 -9
  137. package/dist/language/flows/suite/suite-flakes.js +2 -0
  138. package/dist/language/flows/suite-selection.d.ts +2 -15
  139. package/dist/language/flows/suite-selection.js +4 -3
  140. package/dist/language/flows/verify-change-tools.js +8 -4
  141. package/dist/launch-chromium.d.ts +27 -5
  142. package/dist/launch-chromium.js +38 -8
  143. package/dist/memory/cloud/cloud-config.d.ts +12 -0
  144. package/dist/memory/cloud/cloud-config.js +15 -0
  145. package/dist/memory/cloud/cloud-sync.d.ts +17 -14
  146. package/dist/memory/cloud/cloud-sync.js +12 -1
  147. package/dist/memory/cloud/harness-config.d.ts +26 -1
  148. package/dist/memory/cloud/harness-config.js +99 -4
  149. package/dist/memory/cloud/hud-notices-source.d.ts +21 -0
  150. package/dist/memory/cloud/hud-notices-source.js +128 -0
  151. package/dist/memory/cloud/memory-scope.d.ts +144 -0
  152. package/dist/memory/cloud/memory-scope.js +205 -0
  153. package/dist/memory/cloud/sync-cycle.d.ts +12 -3
  154. package/dist/memory/cloud/sync-cycle.js +16 -3
  155. package/dist/memory/cloud/sync-daemon.js +7 -5
  156. package/dist/memory/cloud/sync-disk.d.ts +1 -1
  157. package/dist/memory/cloud/sync-disk.js +5 -8
  158. package/dist/memory/impact/account-watch.d.ts +19 -0
  159. package/dist/memory/impact/account-watch.js +34 -0
  160. package/dist/memory/impact/impact-recorder.d.ts +8 -0
  161. package/dist/memory/impact/impact-recorder.js +12 -2
  162. package/dist/memory/impact/impact-store.d.ts +16 -0
  163. package/dist/memory/impact/impact-store.js +142 -26
  164. package/dist/memory/intent/intent-shard.d.ts +2 -2
  165. package/dist/memory/intent/intent-tools.js +80 -2
  166. package/dist/memory/journal/envelope.d.ts +8 -8
  167. package/dist/memory/journal/journal-recorder.js +7 -1
  168. package/dist/memory/journal/on-disk/retention.d.ts +3 -0
  169. package/dist/memory/journal/on-disk/retention.js +5 -0
  170. package/dist/memory/journal/on-disk/startup-maintenance.js +3 -1
  171. package/dist/memory/journal/on-disk/workspace-tiers.js +7 -4
  172. package/dist/memory/journal/session-end.js +6 -4
  173. package/dist/memory/project/artifact-root-resolver.d.ts +26 -1
  174. package/dist/memory/project/artifact-root-resolver.js +94 -13
  175. package/dist/memory/project/artifact-root.d.ts +42 -5
  176. package/dist/memory/project/artifact-root.js +87 -8
  177. package/dist/memory/project/dir/reticle-dir.d.ts +4 -2
  178. package/dist/memory/project/dir/reticle-dir.js +3 -1
  179. package/dist/memory/project/project-tools.js +22 -3
  180. package/dist/memory/project/session-root.d.ts +24 -1
  181. package/dist/memory/project/session-root.js +41 -5
  182. package/dist/memory/project/sync-status.d.ts +39 -0
  183. package/dist/memory/project/sync-status.js +90 -0
  184. package/dist/memory/recall/memory-tools.js +2 -0
  185. package/dist/memory/recall/project-memory.d.ts +19 -0
  186. package/dist/memory/recall/project-memory.js +45 -6
  187. package/dist/portal/bridge/bridge.d.ts +3 -1
  188. package/dist/portal/bridge/bridge.js +19 -2
  189. package/dist/portal/bridge/pairing-token.d.ts +9 -0
  190. package/dist/portal/bridge/pairing-token.js +17 -0
  191. package/dist/portal/input/real-input.d.ts +81 -10
  192. package/dist/portal/input/real-input.js +207 -22
  193. package/dist/portal/input/viewport-tools.js +3 -3
  194. package/dist/portal/pool/browser-pool.d.ts +22 -9
  195. package/dist/portal/pool/browser-pool.js +70 -35
  196. package/dist/portal/pool/playwright-launcher.d.ts +13 -0
  197. package/dist/portal/pool/playwright-launcher.js +17 -1
  198. package/dist/portal/pool/pool-contract.d.ts +7 -4
  199. package/dist/portal/session/command-payload.d.ts +8 -0
  200. package/dist/portal/session/command-payload.js +19 -0
  201. package/dist/portal/session/dev-server/dev-command.d.ts +12 -0
  202. package/dist/portal/session/dev-server/dev-command.js +61 -12
  203. package/dist/portal/session/facts/departure-note.d.ts +22 -0
  204. package/dist/portal/session/facts/departure-note.js +47 -0
  205. package/dist/portal/session/facts/session-replaced.d.ts +5 -0
  206. package/dist/portal/session/facts/session-replaced.js +5 -0
  207. package/dist/portal/session/first-run-wiring.d.ts +42 -0
  208. package/dist/portal/session/first-run-wiring.js +132 -0
  209. package/dist/portal/session/human/notes-ledger.d.ts +20 -0
  210. package/dist/portal/session/human/notes-ledger.js +55 -0
  211. package/dist/portal/session/human/review-store.d.ts +12 -0
  212. package/dist/portal/session/human/review-store.js +18 -10
  213. package/dist/portal/session/live-control-tools.d.ts +0 -13
  214. package/dist/portal/session/live-control-tools.js +28 -3
  215. package/dist/portal/session/no-session-diagnosis.d.ts +7 -0
  216. package/dist/portal/session/no-session-diagnosis.js +15 -0
  217. package/dist/portal/session/no-session-next-action.d.ts +11 -0
  218. package/dist/portal/session/no-session-next-action.js +31 -5
  219. package/dist/portal/session/no-session-watch.d.ts +14 -1
  220. package/dist/portal/session/no-session-watch.js +63 -10
  221. package/dist/portal/session/presence/session-mirror.js +3 -0
  222. package/dist/portal/session/serving-directory.d.ts +27 -0
  223. package/dist/portal/session/serving-directory.js +45 -0
  224. package/dist/portal/session/session-health.d.ts +7 -0
  225. package/dist/portal/session/session-health.js +6 -0
  226. package/dist/portal/session/session-info.d.ts +8 -1
  227. package/dist/portal/session/session-info.js +6 -0
  228. package/dist/portal/session/session-manager.d.ts +21 -0
  229. package/dist/portal/session/session-manager.js +44 -8
  230. package/dist/portal/session/session-reaper.js +4 -1
  231. package/dist/portal/session/session-successor.d.ts +5 -0
  232. package/dist/portal/session/session-tools.js +26 -3
  233. package/dist/portal/session/session.d.ts +7 -2
  234. package/dist/portal/session/session.js +21 -18
  235. package/dist/start-options.d.ts +6 -0
  236. package/dist/status-payload.d.ts +10 -0
  237. package/dist/status-payload.js +23 -0
  238. package/dist/surface/http-server.js +11 -1
  239. package/dist/surface/mcp/mcp-proxy.d.ts +5 -0
  240. package/dist/surface/mcp/mcp-proxy.js +36 -5
  241. package/dist/surface/mcp/mcp.d.ts +11 -6
  242. package/dist/surface/mcp/mcp.js +27 -13
  243. package/dist/surface/mcp/server-instructions.d.ts +4 -2
  244. package/dist/surface/mcp/server-instructions.js +14 -4
  245. package/dist/surface/mcp/shared-params.js +2 -2
  246. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +8 -0
  247. package/dist/surface/tools/act/navigation/navigate-arrival.js +56 -2
  248. package/dist/surface/tools/act/settle-in-flight.js +25 -3
  249. package/dist/surface/tools/act-sequence-tool.js +2 -2
  250. package/dist/surface/tools/act-tools.js +23 -7
  251. package/dist/surface/tools/assert/assert-verdict.d.ts +1 -1
  252. package/dist/surface/tools/assert/assert-verdict.js +17 -2
  253. package/dist/surface/tools/browser-tools.js +8 -1
  254. package/dist/surface/tools/chat-drives.d.ts +26 -0
  255. package/dist/surface/tools/chat-drives.js +165 -0
  256. package/dist/surface/tools/empty-session-list.d.ts +41 -0
  257. package/dist/surface/tools/empty-session-list.js +80 -0
  258. package/dist/surface/tools/error-recovery.d.ts +6 -0
  259. package/dist/surface/tools/error-recovery.js +37 -4
  260. package/dist/surface/tools/explore-tools.d.ts +10 -1
  261. package/dist/surface/tools/explore-tools.js +82 -13
  262. package/dist/surface/tools/feedback-tools.js +14 -0
  263. package/dist/surface/tools/harness-explore.d.ts +96 -14
  264. package/dist/surface/tools/harness-explore.js +227 -206
  265. package/dist/surface/tools/harness-plan.d.ts +12 -0
  266. package/dist/surface/tools/harness-plan.js +31 -0
  267. package/dist/surface/tools/harness-script.d.ts +8 -0
  268. package/dist/surface/tools/harness-script.js +342 -0
  269. package/dist/surface/tools/harness-toolset.d.ts +14 -1
  270. package/dist/surface/tools/harness-toolset.js +77 -6
  271. package/dist/surface/tools/invoke-tool.js +52 -1
  272. package/dist/surface/tools/lease-seed.d.ts +29 -0
  273. package/dist/surface/tools/lease-seed.js +123 -0
  274. package/dist/surface/tools/lease-tools.d.ts +3 -23
  275. package/dist/surface/tools/lease-tools.js +30 -131
  276. package/dist/surface/tools/live-call-text.js +2 -2
  277. package/dist/surface/tools/next-step.d.ts +19 -0
  278. package/dist/surface/tools/next-step.js +159 -0
  279. package/dist/surface/tools/observe-tools.js +2 -4
  280. package/dist/surface/tools/platform-moment.d.ts +12 -0
  281. package/dist/surface/tools/platform-moment.js +168 -0
  282. package/dist/surface/tools/query-hint-schema.d.ts +7 -0
  283. package/dist/surface/tools/query-hint-schema.js +10 -0
  284. package/dist/surface/tools/read-tools.js +8 -2
  285. package/dist/surface/tools/real-input-attempt.js +96 -13
  286. package/dist/surface/tools/tool-kit.d.ts +15 -1
  287. package/dist/surface/tools/tool-kit.js +4 -6
  288. package/dist/surface/tools/tools.js +20 -48
  289. package/dist/telemetry/hud-metrics.d.ts +2 -0
  290. package/dist/telemetry/hud-metrics.js +17 -0
  291. package/dist/telemetry/telemetry.d.ts +12 -0
  292. package/dist/telemetry/telemetry.js +43 -14
  293. package/dist/telemetry/verification-of.js +1 -0
  294. package/dist/wire-journal.js +4 -1
  295. package/docs/README.md +1 -1
  296. package/docs/agent-cheatsheet.md +1 -1
  297. package/docs/autodrive.md +29 -25
  298. package/docs/cli/doctor.mdx +6 -0
  299. package/docs/cli/drive.mdx +8 -2
  300. package/docs/cli/init.mdx +11 -13
  301. package/docs/cli/status.mdx +5 -1
  302. package/docs/cli/version.mdx +3 -3
  303. package/docs/cli.mdx +1 -1
  304. package/docs/desktop.mdx +5 -1
  305. package/docs/environment.mdx +3 -7
  306. package/docs/getting-started.md +13 -11
  307. package/docs/human-control.md +1 -2
  308. package/docs/index.mdx +4 -3
  309. package/docs/install-manual.mdx +2 -2
  310. package/docs/packages/tauri.mdx +2 -2
  311. package/docs/packages/vite-plugin.mdx +22 -1
  312. package/docs/packages.mdx +1 -1
  313. package/docs/quickstart.mdx +17 -7
  314. package/docs/telemetry.md +1 -1
  315. package/docs/tools/session-and-feedback.mdx +9 -9
  316. package/docs/usage.md +5 -2
  317. package/docs/what-is-recorded.md +13 -3
  318. package/docs/what-is-reticle.mdx +3 -1
  319. package/package.json +8 -12
  320. package/dist/features/harness/driver.d.ts +0 -50
  321. package/dist/features/harness/driver.js +0 -236
  322. package/dist/features/harness/jev-driver.d.ts +0 -188
  323. package/dist/features/harness/jev-driver.js +0 -678
  324. package/dist/features/harness/openai-driver.d.ts +0 -58
  325. package/dist/features/harness/openai-driver.js +0 -206
  326. package/dist/memory/project/dir/fill-value-store.d.ts +0 -32
  327. package/dist/memory/project/dir/fill-value-store.js +0 -88
  328. package/docs/local-registry.md +0 -143
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import { hudSignIn } from './cli/cloud-login.js';
2
3
  import { handleSetupMcp, handleSetupInstall } from './cli/setup-mcp-cli.js';
3
4
  import { reportStepFromCli, reportTutorialShown } from './cli-onboarding.js';
4
5
  import { runDemoTour } from './cli/demo/demo-run.js';
5
6
  import { renderTutorial } from './cli/tutorial.js';
6
7
  import { pathToFileURL } from 'node:url';
7
- import { openFailureNote } from './cli/answers/open-note.js';
8
+ import { openFailureNote, openLaunchFailureNote } from './cli/answers/open-note.js';
8
9
  import { realpathSync } from 'node:fs';
9
10
  import { join } from 'node:path';
10
11
  import { installCrashGuard } from './setup/setup-command.js';
@@ -15,9 +16,9 @@ import { splitBrainFields, withNextAction } from './cli/status-fields.js';
15
16
  import { reticleStateHome } from './daemon/daemon.js';
16
17
  import { handleMcp } from './cli/mcp-command.js';
17
18
  import { handleReport } from './cli/report-command.js';
18
- import { daemonProjectAt, resolveDaemonForProject } from './daemon/daemon-resolve.js';
19
+ import { daemonProjectAt, readDaemonRegistry, resolveDaemonForProject, } from './daemon/daemon-resolve.js';
19
20
  import { pickDaemonPortToBind } from './daemon/binding/free-port.js';
20
- import { portForInit, portFromEnv } from './setup/init/init-port.js';
21
+ import { isSameOrAbove, portForInit, portFromEnv } from './setup/init/init-port.js';
21
22
  import { daemonStartOptions } from './cli/daemon-start-options.js';
22
23
  import { handleWatch, handleCapsules, handleGate, loadNamedFlows, resolveChangedFiles, } from './cli/cli-flow-commands.js';
23
24
  import { RETICLE_DEFAULT_PORT, ReticleDir, ReticleEnv, devServersForProject, } from '@reticlehq/core';
@@ -78,6 +79,10 @@ async function handleInit(parsed) {
78
79
  },
79
80
  daemonPresent: async (p) => presenceIsUsable(await probePresence(p, { tcpOpen: probeDaemon, status: fetchStatus })),
80
81
  pickPort: (p) => pickDaemonPortToBind(p),
82
+ daemonStartedHere: (p) => {
83
+ const started = readDaemonRegistry(reticleStateHome()).find((e) => e.port === p)?.cwd;
84
+ return started !== undefined && isSameOrAbove(started, cwd);
85
+ },
81
86
  });
82
87
  const io = buildNodeIo(cwd, serverInitHost(), { stderr: true === parsed.json });
83
88
  const result = runInit({
@@ -104,15 +109,18 @@ async function handleInit(parsed) {
104
109
  import { handleServe, handleStop, handleRestart } from './cli/lifecycle/daemon-lifecycle.js';
105
110
  import { statusLines } from './cli/status/status-lines.js';
106
111
  /**
107
- * Log the status event AND print it for a person.
108
- *
109
- * Both, not either: the JSON line is documented and is what a log is for, and the block is for
110
- * whoever is reading the terminal — which, once an agent is running the command, is somebody who
111
- * did not type it and cannot be assumed to parse it.
112
+ * Report status to the terminal. `--json` writes one structured line to STDOUT so it can be piped
113
+ * into `jq`; the human-readable block goes to stdout too. `log()` stays on stderr (the MCP
114
+ * transport lives on stdout), so `--json` must not go through it.
112
115
  */
113
- function reportStatus(fields, json) {
116
+ export function reportStatus(fields, json) {
114
117
  if (json) {
115
- log('reticle_status', fields);
118
+ const line = JSON.stringify({
119
+ t: new Date().toISOString(),
120
+ event: 'reticle_status',
121
+ ...fields,
122
+ });
123
+ process.stdout.write(`${line}\n`);
116
124
  return;
117
125
  }
118
126
  process.stdout.write(`${statusLines(fields).join('\n')}\n`);
@@ -223,14 +231,13 @@ export async function handleStatus(port, json = false) {
223
231
  ...nudge,
224
232
  });
225
233
  }
226
- /** Print the running package version (resolved once in server-version.ts). */
234
+ /**
235
+ * Print the running package version (resolved once in server-version.ts), and nothing else.
236
+ *
237
+ * Bare on stdout so `V=$(reticle version)` works. It also wrote a JSON event to stderr, which a
238
+ * terminal shows: the first thing a new user typed after installing answered with a log line.
239
+ */
227
240
  function handleVersion() {
228
- log('reticle_version', { version: SERVER_VERSION });
229
- // Also on stdout, bare, because every diagnostic here starts by asking which build is running and
230
- // that answer has to be copyable. The event alone looked fine in a terminal -- stderr is on the
231
- // screen too -- but `V=$(reticle version)` came back empty, which is exactly the shape somebody
232
- // reaches for when writing an issue template or a CI check. The event stays; machine-readable
233
- // events all go to stderr and that consistency is worth keeping.
234
241
  process.stdout.write(`${SERVER_VERSION}\n`);
235
242
  }
236
243
  /** `reticle license` — show enterprise activation resolved from the environment (offline; nothing leaves). */
@@ -395,9 +402,7 @@ function handleOpen(requestedPort, url) {
395
402
  // Points at the OS handler, not at `reticle doctor`. Doctor's browser check is about
396
403
  // Reticle's own Chromium, which this command never touches — the same misdirect the
397
404
  // no-session note below spells out at length, left standing here for one more release.
398
- recovery: 'Nothing was opened. This command asks the OS to open a url in your default browser ' +
399
- `(\`${openCommand(decision.url, process.platform).cmd}\` on this platform) and that ` +
400
- 'failed, so the fix is on the OS side: open the url yourself, or set a default browser.',
405
+ recovery: openLaunchFailureNote(decision.url, openCommand(decision.url, process.platform).cmd),
401
406
  });
402
407
  process.exit(1);
403
408
  return;
@@ -431,7 +436,8 @@ function handleOpen(requestedPort, url) {
431
436
  });
432
437
  }
433
438
  function handleDaemonInner(parsed) {
434
- const options = daemonStartOptions(parsed);
439
+ // The daemon the CLI starts is the one that can sign in for the panel: it owns ~/.reticle.
440
+ const options = { ...daemonStartOptions(parsed), hudSignIn };
435
441
  startDaemon(options)
436
442
  .then((server) => {
437
443
  log('reticle_daemon_ready', { port: parsed.port, pid: process.pid });
@@ -722,7 +728,7 @@ export function main() {
722
728
  handleOpen(parsed.port, parsed.url);
723
729
  break;
724
730
  case 'drive':
725
- handleDrive(parsed);
731
+ handleDrive(parsed, readProjectId(process.cwd()));
726
732
  break;
727
733
  case 'verify':
728
734
  handleVerify(parsed);
@@ -15,6 +15,8 @@ interface StatusSession {
15
15
  sessionId: string;
16
16
  url: string;
17
17
  projectId?: string;
18
+ /** The page's own title. Absent when the page has none, or on a daemon too old to send it. */
19
+ title?: string;
18
20
  throttled: boolean;
19
21
  /**
20
22
  * The tab is backgrounded. Reported by the daemon on every session; `false` on one too old to say,
@@ -102,6 +102,7 @@ export function summarizeStatus(payload) {
102
102
  ...('string' === typeof r['projectId'] && 0 < r['projectId'].length
103
103
  ? { projectId: r['projectId'] }
104
104
  : {}),
105
+ ...('string' === typeof r['title'] && 0 < r['title'].length ? { title: r['title'] } : {}),
105
106
  throttled: true === r['throttled'],
106
107
  hidden: true === r['hidden'],
107
108
  stale: true === r['stale'],
@@ -76,6 +76,20 @@ export declare function daemonProjectAt(port: number, home: string): string | un
76
76
  * Only LIVE daemons are named: a port a session used yesterday is history, not a split.
77
77
  */
78
78
  export declare function daemonsServingProjectElsewhere(projectId: string | undefined, myPort: number, home: string, alive: (pid: number) => boolean, connectedOn: (port: number) => boolean): readonly number[];
79
+ /** Live daemons that serve a named project, on any port but `myPort`. */
80
+ export declare function projectDaemonsElsewhere(myPort: number, home: string, alive: (pid: number) => boolean): {
81
+ port: number;
82
+ projectId: string;
83
+ }[];
84
+ /**
85
+ * The sentence for a daemon started outside any project while projects run their own daemons, or
86
+ * `undefined` when there are none. Its sessions are empty because the apps dial their own ports,
87
+ * and "check your scope" is not something an agent inside an editor can act on (#1240).
88
+ */
89
+ export declare function projectlessNote(myPort: number, others: readonly {
90
+ port: number;
91
+ projectId: string;
92
+ }[]): string | undefined;
79
93
  /**
80
94
  * The sentence for a split brain, or `undefined` when there is none.
81
95
  *
@@ -19,6 +19,8 @@
19
19
  import { readdirSync, readFileSync } from 'node:fs';
20
20
  import { join } from 'node:path';
21
21
  import { DaemonRegistryEntrySchema, daemonRegistryPort, pickDaemonPort, } from '@reticlehq/core/artifacts';
22
+ import { ReticleEnv } from '@reticlehq/core';
23
+ const RETICLE_PORT_ENV = ReticleEnv.PORT;
22
24
  /**
23
25
  * Every well-formed registry entry in `home`.
24
26
  *
@@ -117,6 +119,30 @@ export function daemonsServingProjectElsewhere(projectId, myPort, home, alive, c
117
119
  .map((entry) => entry.port)
118
120
  .sort((a, b) => a - b);
119
121
  }
122
+ /** Live daemons that serve a named project, on any port but `myPort`. */
123
+ export function projectDaemonsElsewhere(myPort, home, alive) {
124
+ return readDaemonRegistry(home)
125
+ .filter((entry) => entry.port !== myPort && alive(entry.pid))
126
+ .flatMap((entry) => entry.projectId === undefined || 0 === entry.projectId.length
127
+ ? []
128
+ : [{ port: entry.port, projectId: entry.projectId }])
129
+ .sort((a, b) => a.port - b.port);
130
+ }
131
+ /**
132
+ * The sentence for a daemon started outside any project while projects run their own daemons, or
133
+ * `undefined` when there are none. Its sessions are empty because the apps dial their own ports,
134
+ * and "check your scope" is not something an agent inside an editor can act on (#1240).
135
+ */
136
+ export function projectlessNote(myPort, others) {
137
+ const first = others[0];
138
+ if (first === undefined)
139
+ return undefined;
140
+ const listed = others.map((o) => `${o.projectId} on :${String(o.port)}`).join(', ');
141
+ return (`this daemon on :${String(myPort)} was started outside any project, and the projects on this ` +
142
+ `machine run their own daemons: ${listed}. Their apps connect there, not here. Point the MCP ` +
143
+ `server at one — set ${RETICLE_PORT_ENV}=${String(first.port)} in its config, or launch it from ` +
144
+ 'that project directory.');
145
+ }
120
146
  /**
121
147
  * The sentence for a split brain, or `undefined` when there is none.
122
148
  *
@@ -179,8 +205,13 @@ export async function resolveMcpPort(preferred, projectId, home, deps) {
179
205
  const mine = resolveDaemonForProject(projectId, home, deps.alive);
180
206
  if (mine !== undefined)
181
207
  return mine;
182
- if (!(await deps.daemonPresent(preferred)))
183
- return preferred;
208
+ if (!(await deps.daemonPresent(preferred))) {
209
+ // A caller outside any project (an editor's user-scope MCP, launched from $HOME) used to dial the
210
+ // default and never see the one project that had moved off it (#1240). With exactly one project
211
+ // daemon live, that is the one; with several, the no-session answer names them instead.
212
+ const projects = projectId === undefined ? projectDaemonsElsewhere(preferred, home, deps.alive) : [];
213
+ return 1 === projects.length ? (projects[0]?.port ?? preferred) : preferred;
214
+ }
184
215
  return adoptable(daemonProjectAt(preferred, home), projectId)
185
216
  ? preferred
186
217
  : deps.pickPort(preferred);
@@ -34,7 +34,7 @@ interface AgentWriteResult {
34
34
  */
35
35
  export declare function applyAgentPlan(plan: readonly AgentPlanStep[], io: AgentWriterIo): AgentWriteResult[];
36
36
  /** The `/reticle` instruction, for agents that load skills from a directory. */
37
- export declare const RETICLE_SKILL = "---\nname: reticle\ndescription: Verify a change against the running app, from inside it, before calling it done.\n---\n\nReticle exposes the running app to you as `reticle_*` MCP tools. Look, act, observe, assert.\n\nOnly `reticle_act_and_wait` and `reticle_assert` produce a verdict. Everything else moves or\nreads the app and proves nothing, so a drive that ends without one of those has no result however\nmany tools it used.\n\n`verified: \"unknown\"` is not a pass: it means Reticle could not tell what happened.\n`verified: \"no-fault\"` is not a pass either, because nothing was declared to prove. Never weaken a\ncheck to make it pass.\n\nThe cheapest path that answers the question:\n\n- \"did my edit break anything?\" -> `reticle_verify { action: \"change\", files: [...] }`\n- \"does this known journey still work?\" -> `reticle_verify { action: \"flows\", names: [\"...\"] }`\n- \"does this new behaviour work?\" -> `reticle_act { steps: [...] }` for the setup, then ONE `reticle_act_and_wait`\n- \"nothing is recorded yet\" -> `reticle_verify { action: \"explore\", persona: \"<who does what>\" }`, which\n drives the app and SAVES what it drove, so the next run replays with no model in the loop\n";
37
+ export declare const RETICLE_SKILL = "---\nname: reticle\ndescription: Verify a change against the running app, from inside it, before calling it done.\n---\n\nReticle exposes the running app to you as `reticle_*` MCP tools. Look, act, observe, assert.\n\nOnly `reticle_act_and_wait` and `reticle_assert` produce a verdict. Everything else moves or\nreads the app and proves nothing, so a drive that ends without one of those has no result however\nmany tools it used.\n\n`verified: \"unknown\"` is not a pass: it means Reticle could not tell what happened.\n`verified: \"no-fault\"` is not a pass either, because nothing was declared to prove. Never weaken a\ncheck to make it pass.\n\nThe cheapest path that answers the question:\n\n- \"did my edit break anything?\" -> `reticle_verify { action: \"change\", files: [...] }`\n- \"does this known journey still work?\" -> `reticle_verify { action: \"flows\", names: [\"...\"] }`\n- \"does this new behaviour work?\" -> `reticle_act { steps: [...] }` for the setup, then ONE `reticle_act_and_wait`\n- \"nothing is recorded yet\" -> drive the journey yourself with `reticle_act_and_wait` and an `until` on its\n last step; it is saved as a flow, so the next run replays with no model in the loop. With a linked\n project, `reticle_verify { action: \"explore\", persona: \"<who does what>\" }` drives it for you\n";
38
38
  interface SkillWriteResult {
39
39
  readonly id: string;
40
40
  readonly file: string;
@@ -84,8 +84,9 @@ The cheapest path that answers the question:
84
84
  - "did my edit break anything?" -> \`reticle_verify { action: "change", files: [...] }\`
85
85
  - "does this known journey still work?" -> \`reticle_verify { action: "flows", names: ["..."] }\`
86
86
  - "does this new behaviour work?" -> \`reticle_act { steps: [...] }\` for the setup, then ONE \`reticle_act_and_wait\`
87
- - "nothing is recorded yet" -> \`reticle_verify { action: "explore", persona: "<who does what>" }\`, which
88
- drives the app and SAVES what it drove, so the next run replays with no model in the loop
87
+ - "nothing is recorded yet" -> drive the journey yourself with \`reticle_act_and_wait\` and an \`until\` on its
88
+ last step; it is saved as a flow, so the next run replays with no model in the loop. With a linked
89
+ project, \`reticle_verify { action: "explore", persona: "<who does what>" }\` drives it for you
89
90
  `;
90
91
  /**
91
92
  * Write the skill for agents that have somewhere to put it, and only where they are installed.
@@ -9,6 +9,7 @@
9
9
  /** A lease this run opened, and the way to hand it back; or why it could not be opened. */
10
10
  type LeaseOutcome = {
11
11
  readonly sessionId: string;
12
+ readonly zeroInstall: boolean;
12
13
  readonly release: () => Promise<void>;
13
14
  } | {
14
15
  readonly failed: string;
@@ -25,6 +25,7 @@ export async function openLeaseFor(port, url, token) {
25
25
  }
26
26
  return {
27
27
  sessionId: got.leased,
28
+ zeroInstall: got.zeroInstall,
28
29
  release: async () => {
29
30
  await releaseLease(caller, got.leased);
30
31
  await close();
@@ -59,6 +59,14 @@ export declare const WAIT_CEILING_MS: number;
59
59
  */
60
60
  export declare function judgeWait(facts: WaitFacts): WaitVerdict;
61
61
  export declare function portBusyMessage(output: string): string | undefined;
62
+ /**
63
+ * The dev server died before serving: say so, and quote the end of what it printed.
64
+ *
65
+ * Without the tail the reason was invisible: a missing `vite` binary printed `vite: command not
66
+ * found` to its own log, setup said only "exited without serving anything", and its first remedy
67
+ * ("start the dev server yourself") failed the same way.
68
+ */
69
+ export declare function devServerDiedMessage(output: string): string;
62
70
  export declare function announcedUrl(output: string): string | undefined;
63
71
  /**
64
72
  * The url to watch: whatever the tool announced, else one composed from a port we OBSERVED our own
@@ -61,6 +61,25 @@ export function portBusyMessage(output) {
61
61
  return (`port ${port} is already in use — something else is serving there, so this launcher did not ` +
62
62
  `bind. Pass --url http://localhost:${port} if that process is this app, or free the port and re-run.`);
63
63
  }
64
+ /** How many of the dev server's last lines a failure quotes. Enough for a stack's first frames. */
65
+ const DIED_TAIL_LINES = 6;
66
+ /**
67
+ * The dev server died before serving: say so, and quote the end of what it printed.
68
+ *
69
+ * Without the tail the reason was invisible: a missing `vite` binary printed `vite: command not
70
+ * found` to its own log, setup said only "exited without serving anything", and its first remedy
71
+ * ("start the dev server yourself") failed the same way.
72
+ */
73
+ export function devServerDiedMessage(output) {
74
+ const tail = output
75
+ .split('\n')
76
+ .map((line) => line.trimEnd())
77
+ .filter((line) => 0 < line.trim().length)
78
+ .slice(-DIED_TAIL_LINES);
79
+ return 0 === tail.length
80
+ ? 'The dev server exited without serving anything, and printed nothing.'
81
+ : `The dev server exited without serving anything. The last it printed:\n${tail.map((line) => ` ${line}`).join('\n')}`;
82
+ }
64
83
  /** A url the dev server announced, if it has announced one. Never composed, only read. */
65
84
  const URL_IN_OUTPUT = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):\d+/;
66
85
  export function announcedUrl(output) {
@@ -12,4 +12,8 @@ export declare function portForInit(explicit: number | undefined, configured: nu
12
12
  daemonProjects: (port: number) => Promise<readonly string[]>;
13
13
  daemonPresent: (port: number) => Promise<boolean>;
14
14
  pickPort: (preferred: number) => Promise<number>;
15
+ /** The daemon on `port` was started from this project's directory or one above it. */
16
+ daemonStartedHere?: (port: number) => boolean;
15
17
  }): Promise<number | undefined>;
18
+ /** Is `dir` the same directory as `cwd`, or an ancestor of it? */
19
+ export declare function isSameOrAbove(dir: string, cwd: string): boolean;
@@ -8,6 +8,7 @@
8
8
  *
9
9
  * Undefined means the default.
10
10
  */
11
+ import { relative, isAbsolute } from 'node:path';
11
12
  import { RETICLE_DEFAULT_PORT, ReticleEnv } from '@reticlehq/core';
12
13
  const MAX_PORT = 65_535;
13
14
  /**
@@ -34,6 +35,10 @@ export async function portForInit(explicit, configured, projectId, deps) {
34
35
  // config as a `port` nobody chose.
35
36
  if (!(await deps.daemonPresent(RETICLE_DEFAULT_PORT)))
36
37
  return undefined;
38
+ // Started here — by this project's own editor MCP, typically — so it is ours whatever project id
39
+ // it recorded. Moving off it strands that agent: an MCP resolves its port once, at startup.
40
+ if (true === deps.daemonStartedHere?.(RETICLE_DEFAULT_PORT))
41
+ return undefined;
37
42
  // The bridge's own rule: it refuses a page only when it has served exactly ONE other project. A
38
43
  // daemon serving nobody, several projects, or ours accepts this one. A project that has never
39
44
  // been through init has no id yet, so a single named holder is somebody else.
@@ -42,3 +47,8 @@ export async function portForInit(explicit, configured, projectId, deps) {
42
47
  return undefined;
43
48
  return deps.pickPort(RETICLE_DEFAULT_PORT + 1);
44
49
  }
50
+ /** Is `dir` the same directory as `cwd`, or an ancestor of it? */
51
+ export function isSameOrAbove(dir, cwd) {
52
+ const rel = relative(dir, cwd);
53
+ return '' === rel || (!rel.startsWith('..') && !isAbsolute(rel));
54
+ }
@@ -6,6 +6,16 @@
6
6
  * every command harder to read.
7
7
  */
8
8
  import { type InitResult } from '@reticlehq/init';
9
+ /**
10
+ * Does the served document name the SDK, so its absence means a stale dev server?
11
+ *
12
+ * A Next App Router page names its client components in the flight data inlined into the HTML —
13
+ * `reticle-dev.tsx`, measured on Next 16 under Turbopack and webpack alike. Reported from a Next app
14
+ * whose dev server was already running before init: setup waited on a page that could never connect
15
+ * and said nothing, because Next was assumed to carry the SDK only in its bundle. The Pages Router
16
+ * names chunks, not modules, so it stays silent.
17
+ */
18
+ export declare function htmlCarriesSdk(framework: string, exists: (path: string) => boolean): boolean;
9
19
  /** Just enough of the parsed command to decide and run. */
10
20
  interface InitRuntimeArgs {
11
21
  readonly port: number | undefined;
@@ -6,6 +6,7 @@
6
6
  * every command harder to read.
7
7
  */
8
8
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
9
10
  import { InitConfirmation, RETICLE_DEFAULT_PORT } from '@reticlehq/core';
10
11
  import { FEEDBACK_HINT, Framework, InitFailure } from '@reticlehq/init';
11
12
  import { confirmInstall, nodeConfirmDeps } from '../terminal/confirm.js';
@@ -30,6 +31,22 @@ const POLL_MS = 250;
30
31
  * of the document can never see it. See `htmlCarriesSdk` in run-setup.ts.
31
32
  */
32
33
  const HTML_CARRIES_SDK = new Set([Framework.VITE, Framework.HTML]);
34
+ /** The connect component `init` writes for a Next App Router project. */
35
+ const NEXT_APP_CONNECT = ['app', 'src/app'].flatMap((d) => ['tsx', 'jsx', 'js'].map((ext) => `${d}/reticle-dev.${ext}`));
36
+ /**
37
+ * Does the served document name the SDK, so its absence means a stale dev server?
38
+ *
39
+ * A Next App Router page names its client components in the flight data inlined into the HTML —
40
+ * `reticle-dev.tsx`, measured on Next 16 under Turbopack and webpack alike. Reported from a Next app
41
+ * whose dev server was already running before init: setup waited on a page that could never connect
42
+ * and said nothing, because Next was assumed to carry the SDK only in its bundle. The Pages Router
43
+ * names chunks, not modules, so it stays silent.
44
+ */
45
+ export function htmlCarriesSdk(framework, exists) {
46
+ if (HTML_CARRIES_SDK.has(framework))
47
+ return true;
48
+ return Framework.NEXT === framework && NEXT_APP_CONNECT.some(exists);
49
+ }
33
50
  /** Registration is global MCP wiring, so both flags that disown it are read here. */
34
51
  function wantsAgents(parsed) {
35
52
  return false !== parsed.agents && false !== parsed.mcp;
@@ -160,7 +177,7 @@ export async function continueAfterInit(parsed, result, io, cwd) {
160
177
  startupBudgetMs: parsed.timeoutSeconds * 1000,
161
178
  }),
162
179
  pollMs: POLL_MS,
163
- htmlCarriesSdk: HTML_CARRIES_SDK.has(context.framework),
180
+ htmlCarriesSdk: htmlCarriesSdk(context.framework, (rel) => existsSync(join(context.appDir, rel))),
164
181
  // A plain page with no dev script gets Reticle's own static server, so the page is actually
165
182
  // served and the connect can be proved, instead of stopping at "start the app yourself".
166
183
  ...(() => {
@@ -106,7 +106,8 @@ export class OwnedDevServer {
106
106
  catch {
107
107
  /* not written yet */
108
108
  }
109
- return Date.now() - lastOutputAt;
109
+ // Filesystem timestamps can include a fractional millisecond beyond Date.now()'s integer.
110
+ return Math.max(0, Date.now() - lastOutputAt);
110
111
  }
111
112
  /** Ports anything in this server's process tree is listening on. */
112
113
  listeningPorts() {
@@ -34,7 +34,8 @@ export function remainingSteps(p) {
34
34
  '`reticle_*` tools, ending in `reticle_act_and_wait` or `reticle_assert` — those two are ' +
35
35
  'what produce a verdict. Or hand the whole drive to Reticle with `reticle_verify { action: ' +
36
36
  '"explore", persona: "<who does what>" }`, which records what it drove so later runs replay ' +
37
- 'with no model in the loop; that route needs ANTHROPIC_API_KEY.');
37
+ 'with no model in the loop; that route runs on the Reticle platform and needs a linked ' +
38
+ 'project (`reticle connect`); every plan, Free included, has monthly Harness credits.');
38
39
  }
39
40
  // Whether anything is actually outstanding, read BEFORE the docs pointer is added -- that line is
40
41
  // an offer, not a task, and a run with nothing left still gets it.
@@ -55,6 +55,11 @@ export interface DaemonReason {
55
55
  export interface OwnedBrowser {
56
56
  readonly sessionId: string;
57
57
  readonly release: () => Promise<void>;
58
+ /**
59
+ * The page never dialled in, so the lease injected Reticle's own reader. That session proves the
60
+ * reader, not this install, and must never end onboarding as "connected".
61
+ */
62
+ readonly zeroInstall?: boolean;
58
63
  }
59
64
  export interface SetupEffects {
60
65
  /** Start the dev server. Resolves once started; the caller owns stopping it. */
@@ -13,7 +13,7 @@
13
13
  * way of going wrong, and the alternative to injecting them is a test that boots a real dev server
14
14
  * and a real browser to find out what happens when neither works.
15
15
  */
16
- import { judgeWait, portBusyMessage, QUIET_MEANS_HUNG_MS, urlToWatch, WaitVerdict, } from './bringup/dev-server-wait.js';
16
+ import { judgeWait, devServerDiedMessage, portBusyMessage, QUIET_MEANS_HUNG_MS, urlToWatch, WaitVerdict, } from './bringup/dev-server-wait.js';
17
17
  import { connectProgressLine, waitProgressLine } from './terminal/wait-progress.js';
18
18
  /**
19
19
  * Windows gets longer to say something before silence counts as a wedge.
@@ -177,18 +177,20 @@ export async function runSetupPhases(input, fx) {
177
177
  break;
178
178
  }
179
179
  if (WaitVerdict.DEAD === verdict) {
180
- note(portBusyMessage(fx.devServerOutput()) ??
181
- 'The dev server exited without serving anything.');
180
+ note(portBusyMessage(fx.devServerOutput()) ?? devServerDiedMessage(fx.devServerOutput()));
182
181
  return stop(input, SetupPhase.DEV_SERVER, {}, notes);
183
182
  }
184
183
  if (WaitVerdict.HUNG === verdict) {
185
184
  // Says BOTH were checked. A server that prints nothing but IS listening is the CRA case and
186
185
  // must not be failed, so a reader has to be able to tell "we looked at the log" from "we
187
186
  // looked at the log AND the ports".
188
- note((input.startupBudgetMs !== undefined && fx.now() - startedAt >= input.startupBudgetMs
189
- ? `The dev server did not become ready within ${String(input.startupBudgetMs)}ms. `
190
- : undefined === watching
191
- ? 'The dev server neither printed a URL nor bound a port, so setup has nothing to open. '
187
+ const within = input.startupBudgetMs !== undefined && fx.now() - startedAt >= input.startupBudgetMs
188
+ ? ` within ${String(input.startupBudgetMs)}ms`
189
+ : undefined;
190
+ note((undefined === watching
191
+ ? `The dev server neither printed a URL nor bound a port${within ?? ''}, so setup has nothing to open. `
192
+ : undefined !== within
193
+ ? `The dev server did not become ready${within}. `
192
194
  : `The dev server stopped making progress before ${watching} became ready. `) +
193
195
  'Check its log, increase --timeout for a slow build, or pass --url with the address it serves.');
194
196
  return stop(input, SetupPhase.DEV_SERVER, {}, notes);
@@ -232,6 +234,23 @@ export async function runSetupPhases(input, fx) {
232
234
  // dialled the bridge" over a correct install. With a lease to fall back on it is entered too, and
233
235
  // only the system browser is skipped.
234
236
  const canLease = undefined !== fx.openLease;
237
+ const injected = new Set();
238
+ /**
239
+ * The lease to keep, or undefined. A lease whose page only connected through Reticle's injected
240
+ * reader is handed straight back and its session excluded: it would otherwise be picked below and
241
+ * report a plain server with no SDK as an instrumented app.
242
+ */
243
+ const keepLease = async (owned) => {
244
+ if ('failed' in owned) {
245
+ note(`Could not open ${url} in a Reticle-owned browser either: ${owned.failed}`);
246
+ return undefined;
247
+ }
248
+ if (true !== owned.zeroInstall)
249
+ return owned;
250
+ injected.add(owned.sessionId);
251
+ await releaseLease(owned);
252
+ return undefined;
253
+ };
235
254
  if (null === alreadyConnected && policy.openBrowser && (input.openBrowser || canLease)) {
236
255
  // SERVED is the precondition, not SDK_PRESENT: a url that answers nothing is the only state
237
256
  // where a window is certainly useless. Whether the SDK is in the served HTML is a DIFFERENT
@@ -260,14 +279,9 @@ export async function runSetupPhases(input, fx) {
260
279
  const launcherFailed = input.openBrowser ? await fx.openBrowser(url) : undefined;
261
280
  openedBrowser = input.openBrowser;
262
281
  if (undefined !== fx.openLease && (!input.openBrowser || undefined !== launcherFailed)) {
263
- const owned = await fx.openLease(url);
264
- if ('failed' in owned) {
265
- note(`Could not open ${url} in a Reticle-owned browser either: ${owned.failed}`);
266
- }
267
- else {
268
- lease = owned;
282
+ lease = await keepLease(await fx.openLease(url));
283
+ if (undefined !== lease)
269
284
  openedBrowser = true;
270
- }
271
285
  }
272
286
  }
273
287
  }
@@ -288,7 +302,8 @@ export async function runSetupPhases(input, fx) {
288
302
  while (null === session) {
289
303
  // On a desktop app, only the desktop window counts. AppShape and the runtime a page reports use
290
304
  // the same three names, so the shape IS the requirement — see session-pick.
291
- session = pickSession(await fx.listSessions(), url, before, requiredRuntime);
305
+ const listedNow = (await fx.listSessions()).filter((s) => !injected.has(s.sessionId));
306
+ session = pickSession(listedNow, url, before, requiredRuntime);
292
307
  if (null !== session)
293
308
  break;
294
309
  if (deadline <= fx.now())
@@ -301,13 +316,7 @@ export async function runSetupPhases(input, fx) {
301
316
  !isDesktop(input.shape) &&
302
317
  fx.now() - waitStartedAt >= SYSTEM_BROWSER_GRACE_MS) {
303
318
  leaseFallbackTried = true;
304
- const owned = await fx.openLease(url);
305
- if ('failed' in owned) {
306
- note(`Could not open ${url} in a Reticle-owned browser either: ${owned.failed}`);
307
- }
308
- else {
309
- lease = owned;
310
- }
319
+ lease = await keepLease(await fx.openLease(url));
311
320
  continue;
312
321
  }
313
322
  // Not in silence: a desktop run used to print one line and then nothing for ten minutes.
@@ -368,9 +377,8 @@ export async function runSetupPhases(input, fx) {
368
377
  // `<url>` stays a placeholder in this string on purpose: `guidance-commands-run` feeds every
369
378
  // command we print to the real parser, and an interpolated value reads there as a missing operand.
370
379
  // The live url is named in the sentence instead, where a reader needs it anyway.
371
- // Both routes, because `explore` needs a model: without ANTHROPIC_API_KEY it answers "No model
372
- // configured to drive the app". Naming only that one hands the reader a dead end on any machine
373
- // without a key, which is the same defect this release spent its time removing everywhere else.
380
+ // Both routes, because `explore` runs on the platform: without a linked project it refuses.
381
+ // Naming only that one hands the reader a dead end on any machine without one.
374
382
  // A heading and a numbered list rather than one paragraph: a reader scans for what to do next.
375
383
  //
376
384
  // This was a single 524-character note, measured on a real first run: eighty-three words, no
@@ -390,8 +398,8 @@ export async function runSetupPhases(input, fx) {
390
398
  note(' 1. Drive one flow and end it with `reticle_act_and_wait` or `reticle_assert`. Those two ' +
391
399
  'produce a verdict; nothing else does.');
392
400
  note(' 2. Or hand over the whole drive: `reticle_verify { action: "explore", persona: "<who does ' +
393
- 'what>" }` records what it drove, so later runs replay with no model in the loop. Needs ' +
394
- 'ANTHROPIC_API_KEY.');
401
+ 'what>" }` records what it drove, so later runs replay with no model in the loop. Runs on ' +
402
+ 'the Reticle platform: needs a linked project (`reticle connect`); Free includes monthly Harness credits.');
395
403
  return {
396
404
  ok: true,
397
405
  reachedPhase: SetupPhase.CONNECT,
@@ -30,7 +30,8 @@ interface SetupCommandInput extends Omit<SetupInput, 'shape'> {
30
30
  /** The bridge's pairing token, for the MCP transport a connect-proof lease is opened over. */
31
31
  readonly pairingToken?: string | undefined;
32
32
  }
33
- export declare function registerOtherAgents(print: (line: string) => void): void;
33
+ /** Returns how many agents it wrote a config for, so a caller does not then say "none found". */
34
+ export declare function registerOtherAgents(print: (line: string) => void): number;
34
35
  type SetupCommandResult = SetupOutcome;
35
36
  export declare function runSetupCommand(input: SetupCommandInput, print: (line: string) => void): Promise<SetupCommandResult>;
36
37
  export {};
@@ -89,6 +89,7 @@ function hasElectronDependency(dir) {
89
89
  */
90
90
  /** `agent`, `agents`. */
91
91
  const agentWord = (n) => (1 === n ? 'agent' : 'agents');
92
+ /** Returns how many agents it wrote a config for, so a caller does not then say "none found". */
92
93
  export function registerOtherAgents(print) {
93
94
  const platform = process.platform;
94
95
  const home = homedir();
@@ -124,6 +125,7 @@ export function registerOtherAgents(print) {
124
125
  for (const noted of approvals.filter((a) => undefined !== a.warn)) {
125
126
  print(` ⚠ ${noted.name}: ${String(noted.warn)}`);
126
127
  }
128
+ return wrote.length;
127
129
  }
128
130
  /**
129
131
  * Run the phases against the real world.
@@ -22,6 +22,7 @@
22
22
  * the wire changes and stays put when internals do. Versions still ride along, because they are what
23
23
  * a human acts on.
24
24
  */
25
+ import { RETIRED_WIRE_NAMES } from '@reticlehq/core';
25
26
  import { resolveSdkFix } from './sdk-fix.js';
26
27
  /** Which pair disagreed. Named so the nudge can report each independently. */
27
28
  export const SkewPair = {
@@ -38,13 +39,15 @@ export const SkewPair = {
38
39
  * is not a browser to do -- there is no touch equivalent of a console error. A peer speaking a name
39
40
  * we do not know was built against a contract this daemon does not have, and that is a genuine
40
41
  * mismatch: one of us is stale, and tools will behave in ways neither side reports.
42
+ *
43
+ * A name we RETIRED is not unknown: an older peer still lists it, and we never send it (#1343).
41
44
  */
42
45
  function namesWeDoNotKnow(peer, self) {
43
46
  const unknown = [
44
47
  ...peer.commands.filter((name) => !self.commands.includes(name)),
45
48
  ...peer.events.filter((name) => !self.events.includes(name)),
46
49
  ...peer.actions.filter((name) => !self.actions.includes(name)),
47
- ];
50
+ ].filter((name) => !RETIRED_WIRE_NAMES.includes(name));
48
51
  return [...new Set(unknown)].sort();
49
52
  }
50
53
  function versionPhrase(peer, self) {
@@ -77,8 +80,14 @@ export function describeSkew(peer, self) {
77
80
  const unknown = namesWeDoNotKnow(peer.contractParts, self.contractParts);
78
81
  if (0 === unknown.length)
79
82
  return undefined;
83
+ // Direction comes from the versions when both are known. An older peer naming something we do
84
+ // not know has a name we removed without retiring it, not a newer contract.
85
+ const order = compareVersions(peer.version, self.version);
86
+ const why = order !== undefined && order < 0
87
+ ? 'though it is the older of the two — so this daemon dropped a name it still uses'
88
+ : 'so it was built against a newer contract than this one';
80
89
  return (`version skew: ${versionPhrase(peer, self)}, and ${peer.what} speaks names this daemon does ` +
81
- `not know (${unknown.join(', ')}) — so it was built against a newer contract than this one. ` +
90
+ `not know (${unknown.join(', ')}) — ${why}. ` +
82
91
  `Tools will behave in ways neither side reports. ${peer.fix}`);
83
92
  }
84
93
  if (peer.contract === undefined) {