@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
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { readFileSync } from 'node:fs';
11
11
  import { dirname, join, resolve } from 'node:path';
12
- import { SILENT_HOST, buildNodeIo, deriveProjectId, findWorkspaceApps, packageName, } from '@reticlehq/init';
12
+ import { SILENT_HOST, buildNodeIo, deriveProjectId, findWorkspaceApps, isDesktopProject, packageName, } from '@reticlehq/init';
13
13
  import { asProjectId } from '@reticlehq/core';
14
14
  /** The project config `reticle init` writes. */
15
15
  export const RETICLE_CONFIG_BASENAME = '.reticle.json';
@@ -35,18 +35,15 @@ const MAX_CONFIG_SEARCH_DEPTH = 6;
35
35
  * Returns undefined when nothing is found, which keeps every caller's existing default intact.
36
36
  */
37
37
  export function findProjectConfig(cwd) {
38
+ const dir = findProjectConfigDir(cwd);
39
+ return dir === undefined ? undefined : readConfigAt(dir);
40
+ }
41
+ /** The directory holding the nearest `.reticle.json` at or above `cwd`, by the same walk. */
42
+ function findProjectConfigDir(cwd) {
38
43
  let dir = resolve(cwd);
39
44
  for (let depth = 0; depth <= MAX_CONFIG_SEARCH_DEPTH; depth += 1) {
40
- try {
41
- const raw = readFileSync(`${dir}/${RETICLE_CONFIG_BASENAME}`, 'utf8');
42
- const config = JSON.parse(raw);
43
- if ('object' === typeof config && config !== null && !Array.isArray(config)) {
44
- return config;
45
- }
46
- }
47
- catch {
48
- // Absent, unreadable, or not JSON at this level — keep walking.
49
- }
45
+ if (readConfigAt(dir) !== undefined)
46
+ return dir;
50
47
  const parent = dirname(dir);
51
48
  if (parent === dir)
52
49
  break;
@@ -54,6 +51,48 @@ export function findProjectConfig(cwd) {
54
51
  }
55
52
  return undefined;
56
53
  }
54
+ /**
55
+ * The directory of the project this invocation serves: where its `.reticle.json` is, else the one
56
+ * wired workspace app below a monorepo root, else `cwd` itself. What the CLI reads the installed SDK
57
+ * from, so it answers for the app the port above belongs to rather than for the directory it was
58
+ * started in.
59
+ */
60
+ export function projectDirOf(cwd) {
61
+ const configDir = findProjectConfigDir(cwd);
62
+ if (configDir !== undefined)
63
+ return appPointedAtBy(configDir) ?? configDir;
64
+ const apps = workspaceAppPorts(cwd);
65
+ const only = 1 === apps.length ? apps[0] : undefined;
66
+ return only === undefined ? resolve(cwd) : join(resolve(cwd), only.app);
67
+ }
68
+ /**
69
+ * The one workspace app below `dir` whose own config names the project `dir`'s does, or undefined.
70
+ *
71
+ * `init` writes a copy of the app's `.reticle.json` at the root the agent runs from, so the root's
72
+ * config is a pointer to that app rather than a project of its own. The projectId is the link — no
73
+ * path is stored — and a root whose id no app shares, or several do, is its own project.
74
+ */
75
+ function appPointedAtBy(dir) {
76
+ const id = readConfigAt(dir)?.['projectId'];
77
+ if ('string' !== typeof id || 0 === id.length)
78
+ return undefined;
79
+ const named = workspaceApps(dir).filter((app) => id === readConfigAt(join(dir, app))?.['projectId']);
80
+ const only = 1 === named.length ? named[0] : undefined;
81
+ return only === undefined ? undefined : join(dir, only);
82
+ }
83
+ /** The `.reticle.json` in exactly `dir` — no walk — or undefined when absent or not an object. */
84
+ function readConfigAt(dir) {
85
+ try {
86
+ const config = JSON.parse(readFileSync(join(dir, RETICLE_CONFIG_BASENAME), 'utf8'));
87
+ if ('object' === typeof config && config !== null && !Array.isArray(config)) {
88
+ return config;
89
+ }
90
+ }
91
+ catch {
92
+ // Absent, unreadable, or not JSON at this level.
93
+ }
94
+ return undefined;
95
+ }
57
96
  /**
58
97
  * The dev-server port heuristics live in `@reticlehq/init` — `existing-config.ts` is what diagnoses
59
98
  * a `.reticle.json` whose `port` is the app's own dev-server port, and the scaffolder may not import
@@ -61,15 +100,70 @@ export function findProjectConfig(cwd) {
61
100
  * diagnosis, and `cli.ts`) are unchanged.
62
101
  */
63
102
  export { DEV_SERVER_PORTS, isLikelyDevServerPort, devServerPortWarning } from '@reticlehq/init';
103
+ /** A config's `port`, when it is a usable TCP port. */
104
+ function configPort(config) {
105
+ const p = config?.['port'];
106
+ if ('number' === typeof p && Number.isInteger(p) && p > 0 && p < 65536)
107
+ return p;
108
+ return undefined;
109
+ }
64
110
  /**
65
111
  * Read the port stored in the project's .reticle.json (written by `reticle init`).
66
112
  * Returns undefined if the file is absent, unreadable, or has no valid numeric port.
113
+ *
114
+ * With no config at or above `cwd`, it looks DOWN, the same way `projectIdsAt` does. Running from a
115
+ * monorepo root whose app was wired in `apps/web` used to find nothing and fall back to the default
116
+ * port, so `status`, `mcp` and `verify` quietly talked to another project's daemon. One wired app
117
+ * below, or several that agree, give their port; several that disagree give none, and
118
+ * `workspacePortConflict` is what says so rather than letting the default win in silence.
67
119
  */
68
120
  export function readProjectPort(cwd) {
69
- const p = findProjectConfig(cwd)?.['port'];
70
- if ('number' === typeof p && Number.isInteger(p) && p > 0 && p < 65536)
71
- return p;
72
- return undefined;
121
+ const config = findProjectConfig(cwd);
122
+ if (config !== undefined)
123
+ return configPort(config);
124
+ const ports = new Set(workspaceAppPorts(cwd).map((a) => a.port));
125
+ return 1 === ports.size ? [...ports][0] : undefined;
126
+ }
127
+ /** Each workspace app under `cwd` whose own `.reticle.json` records a port. */
128
+ function workspaceAppPorts(cwd) {
129
+ const found = [];
130
+ for (const app of workspaceApps(cwd)) {
131
+ // The app's OWN file only — walking up from an unwired sibling would reach the root, or past it.
132
+ const port = configPort(readConfigAt(join(cwd, app)));
133
+ if (port !== undefined)
134
+ found.push({ app, port });
135
+ }
136
+ return found;
137
+ }
138
+ /** init's workspace discovery under `cwd`, relative app directories; empty when `cwd` is no package. */
139
+ function workspaceApps(cwd) {
140
+ // Only from a directory that is itself a package: a root with no package.json is not a workspace,
141
+ // and scanning, say, a home directory would adopt the port of whichever checkout happened to sit
142
+ // there.
143
+ if (packageNameAt(cwd) === undefined)
144
+ return [];
145
+ try {
146
+ return findWorkspaceApps(buildNodeIo(cwd, SILENT_HOST));
147
+ }
148
+ catch {
149
+ // Discovery touches the filesystem; an unreadable tree is simply no further evidence.
150
+ return [];
151
+ }
152
+ }
153
+ /**
154
+ * The sentence for a monorepo root whose wired apps name different ports, or undefined when there is
155
+ * nothing to choose between. `readProjectPort` refuses to pick one; this is what tells the person
156
+ * which ports exist and how to name the one they meant.
157
+ */
158
+ export function workspacePortConflict(cwd) {
159
+ if (findProjectConfig(cwd) !== undefined)
160
+ return undefined;
161
+ const apps = workspaceAppPorts(cwd);
162
+ if (new Set(apps.map((a) => a.port)).size < 2)
163
+ return undefined;
164
+ const listed = apps.map((a) => `${a.app} (:${String(a.port)})`).join(', ');
165
+ return (`This workspace has more than one app wired for Reticle, on different ports: ${listed}. ` +
166
+ 'Not guessing which one you meant — run the command from inside that app, or pass --port.');
73
167
  }
74
168
  /**
75
169
  * Read the stable projectId stored in the project's .reticle.json (written by `reticle init`). The daemon
@@ -142,6 +236,22 @@ export function readProjectFramework(cwd) {
142
236
  return framework;
143
237
  return undefined;
144
238
  }
239
+ /**
240
+ * Whether the project this directory serves is a desktop shell (Electron, Tauri) — the same question
241
+ * `doctor` asks, answered from the same files. Read so the no-session guidance stops sending a
242
+ * desktop user to a browser URL.
243
+ */
244
+ export function readProjectIsDesktop(cwd) {
245
+ const dir = projectDirOf(cwd);
246
+ return isDesktopProject((relative) => {
247
+ try {
248
+ return readFileSync(join(dir, relative), 'utf8');
249
+ }
250
+ catch {
251
+ return undefined;
252
+ }
253
+ });
254
+ }
145
255
  /**
146
256
  * Whether the durable causal journal is enabled. On by default (the journal IS the loop); off only via
147
257
  * explicit opt-out — `.reticle.json` `"journal": false`, or `RETICLE_JOURNAL` set to `0`/`false`. The env
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
- import { probeCli } from '@reticlehq/init';
4
+ import { claudeAddCommand, McpClient, probeCli, terminalWidth, wrapForTerminal, } from '@reticlehq/init';
5
5
  import { installClosing, tutorialShownSteps } from './tutorial.js';
6
6
  import { setupMcp, knownClientLabels } from '../setup/setup-mcp.js';
7
7
  import { reportInstallSteps } from '../setup/setup-install.js';
@@ -22,7 +22,7 @@ export function handleSetupMcp(reportStep, standalone = true) {
22
22
  writeFileSync(p, contents);
23
23
  },
24
24
  homeDir: () => homedir(),
25
- print: (line) => process.stdout.write(`${line}\n`),
25
+ print: (line) => process.stdout.write(`${wrapForTerminal(line, terminalWidth())}\n`),
26
26
  reportStep,
27
27
  // `probeCli`, not a bare `execFileSync`: on Windows `claude` is a `.cmd` shim that cannot be
28
28
  // spawned without a shell, so the plain call threw ENOENT for every Windows user and the
@@ -46,16 +46,24 @@ export function handleSetupMcp(reportStep, standalone = true) {
46
46
  return;
47
47
  }
48
48
  for (const id of result.registered)
49
- io.print(` registered ${id}`);
49
+ io.print(` ✓ ${id}`);
50
50
  for (const id of result.alreadyThere)
51
- io.print(` already ${id}`);
51
+ io.print(` ✓ ${id} (already set up)`);
52
+ for (const id of result.failed) {
53
+ io.print(` ⚠ ${id}: Reticle registration failed; this agent may not have the tools.`);
54
+ io.print(id === McpClient.CLAUDE_CODE
55
+ ? ` Run this command to see the error and retry: ${claudeAddCommand().display}`
56
+ : ' Check your agent config file permissions, then retry: reticle setup mcp');
57
+ }
58
+ if (result.failed.length > 0)
59
+ process.exitCode = 1;
52
60
  // Said, not skipped. These are the clients whose format we will not rewrite, and they used to be
53
61
  // counted as registered: the line below is the difference between a user who adds four lines to
54
62
  // one file and a user who believes they are set up and has no tools.
55
63
  for (const client of result.manual) {
56
- io.print(` BY HAND ${client.id}: add the reticle entry to ${client.configPath}`);
64
+ io.print(` ⚠ ${client.id}: add the reticle entry to ${client.configPath} by hand`);
57
65
  if (client.docs !== undefined)
58
- io.print(` see ${client.docs}`);
66
+ io.print(` see ${client.docs}`);
59
67
  }
60
68
  /*
61
69
  * The OTHER agents, which `init` already reaches and this did not.
@@ -75,13 +83,13 @@ export function handleSetupMcp(reportStep, standalone = true) {
75
83
  });
76
84
  // Said on every run, attended or not: a registration only takes effect when the agent re-reads
77
85
  // its config, and an agent that was already open is the commonest reason "it did not work".
78
- io.print('');
79
- io.print('Restart your agent for it to pick up the new server.');
80
- // Only when this IS the whole command. Inside the installer the tour follows, and its own closing
81
- // already names `reticle init` — printing it here too put the same instruction twice, ten lines
82
- // apart, with the tour sandwiched between them.
83
- if (standalone)
86
+ // Only when this IS the whole command. Inside the installer the restart is the first line of the
87
+ // closing's Next list, where it belongs with the other things to do, not between the stages.
88
+ if (standalone) {
89
+ io.print('');
90
+ io.print('Restart your agent for it to pick up the new server.');
84
91
  io.print('Then: cd <your project> && reticle init');
92
+ }
85
93
  }
86
94
  /**
87
95
  * `reticle setup install` — the Node half of the one-line installer.
@@ -100,7 +108,7 @@ export function handleSetupInstall(opts, reportStep) {
100
108
  }
101
109
  // Installation and onboarding are one script, so the tour prints here and is not gated on
102
110
  // anything — see installClosing for the heuristic this replaced and why it reached nobody.
103
- process.stdout.write(`${installClosing()}\n`);
111
+ process.stdout.write(`${wrapForTerminal(installClosing(opts.mcp), terminalWidth())}\n`);
104
112
  // Shown, so the ONBOARD steps are a fact rather than a guess.
105
113
  for (const step of tutorialShownSteps())
106
114
  reportStep(step);
@@ -76,7 +76,7 @@ export declare function tutorialScript(audience: TutorialAudience): TutorialStep
76
76
  * Split from `renderTutorial` so the closing block is a value a caller can assert on, rather than a
77
77
  * string only findable by matching rendered prose.
78
78
  */
79
- export declare function tutorialNextSteps(audience: TutorialAudience): string;
79
+ export declare function tutorialNextSteps(audience: TutorialAudience, restartAgent?: boolean): string;
80
80
  /**
81
81
  * What the installer prints when it has finished.
82
82
  *
@@ -87,7 +87,7 @@ export declare function tutorialNextSteps(audience: TutorialAudience): string;
87
87
  * It ends with the tour's OWN closing, which already names `reticle init`. Adding a second "Next:"
88
88
  * under it would be the same instruction twice in ten lines.
89
89
  */
90
- export declare function installClosing(): string;
90
+ export declare function installClosing(registered?: boolean): string;
91
91
  export declare function renderTutorial(audience: TutorialAudience): string;
92
92
  export type DemoPlan = {
93
93
  ok: true;
@@ -91,7 +91,7 @@ export function tutorialScript(audience) {
91
91
  * Split from `renderTutorial` so the closing block is a value a caller can assert on, rather than a
92
92
  * string only findable by matching rendered prose.
93
93
  */
94
- export function tutorialNextSteps(audience) {
94
+ export function tutorialNextSteps(audience, restartAgent = false) {
95
95
  const lines = TutorialAudience.AGENT === audience
96
96
  ? [
97
97
  'Next:',
@@ -101,12 +101,15 @@ export function tutorialNextSteps(audience) {
101
101
  ]
102
102
  : [
103
103
  'Next:',
104
- ' cd <your project> && reticle init # wire Reticle into the app',
104
+ ...(restartAgent
105
+ ? [' restart your agent # it loads new tools only when it starts']
106
+ : []),
107
+ ' cd <your project> && reticle init # wire Reticle into the app',
105
108
  ' npm run dev # then load it in a browser',
106
109
  ' reticle status # confirms the app connected, or says why not',
107
110
  '',
108
- 'Then ask your agent to drive one real flow and report the verdict. That is the install',
109
- 'proving itself, and it is the agent’s job rather than a command you run.',
111
+ // One line: the terminal wraps it at the reader's width, not ours.
112
+ 'Then ask your agent to drive one real flow and report the verdict. That is the install proving itself, and it is the agent’s job rather than a command you run.',
110
113
  ];
111
114
  return lines.join('\n');
112
115
  }
@@ -120,14 +123,25 @@ export function tutorialNextSteps(audience) {
120
123
  * It ends with the tour's OWN closing, which already names `reticle init`. Adding a second "Next:"
121
124
  * under it would be the same instruction twice in ten lines.
122
125
  */
123
- export function installClosing() {
126
+ export function installClosing(registered = true) {
127
+ // One line a step. The reasons are for `reticle tutorial`; right after an install they turned a
128
+ // four-step idea into twenty-five lines of prose between the reader and the next command.
129
+ const steps = tutorialScript(TutorialAudience.HUMAN).map((step, index) => ` ${String(index + 1)}. ${INSTALL_SAYS[step.id] ?? step.say}`);
124
130
  return [
125
131
  '',
126
- 'Reticle is installed. Here is what it does, in four steps:',
132
+ '✓ Reticle is installed. How it works:',
133
+ ...steps,
127
134
  '',
128
- renderTutorial(TutorialAudience.HUMAN),
135
+ tutorialNextSteps(TutorialAudience.HUMAN, registered),
129
136
  ].join('\n');
130
137
  }
138
+ /**
139
+ * Tour steps reworded for a reader who has no app wired yet. The in-app tour's "That panel is
140
+ * Reticle" points at a panel on screen; straight after an install there is none to point at.
141
+ */
142
+ const INSTALL_SAYS = {
143
+ connect: 'Once `reticle init` wires your app, a Reticle panel appears in it: your proof the SDK is connected.',
144
+ };
131
145
  export function renderTutorial(audience) {
132
146
  const steps = tutorialScript(audience)
133
147
  .map((step, index) => {
@@ -15,7 +15,9 @@ import { splitBrainFields, withNextAction } from './cli/status-fields.js';
15
15
  import { reticleStateHome } from './daemon/daemon.js';
16
16
  import { handleMcp } from './cli/mcp-command.js';
17
17
  import { handleReport } from './cli/report-command.js';
18
- import { resolveDaemonForProject } from './daemon/daemon-resolve.js';
18
+ import { daemonProjectAt, resolveDaemonForProject } from './daemon/daemon-resolve.js';
19
+ import { pickDaemonPortToBind } from './daemon/binding/free-port.js';
20
+ import { portForInit, portFromEnv } from './setup/init/init-port.js';
19
21
  import { daemonStartOptions } from './cli/daemon-start-options.js';
20
22
  import { handleWatch, handleCapsules, handleGate, loadNamedFlows, resolveChangedFiles, } from './cli/cli-flow-commands.js';
21
23
  import { RETICLE_DEFAULT_PORT, ReticleDir, ReticleEnv, devServersForProject, } from '@reticlehq/core';
@@ -49,19 +51,38 @@ import { continueAfterInit } from './setup/init/init-runtime.js';
49
51
  import { handleDoctor } from './cli/cli-doctor.js';
50
52
  import { serverInitHost } from './setup/init/init-host.js';
51
53
  import { describeLicense } from '../features/license/license.js';
52
- import { isLikelyDevServerPort, devServerPortWarning, readProjectPort, readProjectId, } from './cli/ports/resolve/cli-port.js';
53
- import { DAEMON_INNER_COMMAND, PORT_FLAG, parseCliArgs, CLI_USAGE } from './cli/cli-parse.js';
54
+ import { isLikelyDevServerPort, devServerPortWarning, readProjectPort, readProjectId, workspacePortConflict, projectDirOf, } from './cli/ports/resolve/cli-port.js';
55
+ import { nodeSpawner, readTextFile, reexecAtVersion, versionMatchNote, versionToMatch, } from './cli/launch/sdk-version-match.js';
56
+ import { DAEMON_INNER_COMMAND, PORT_FLAG, parseCliArgs, CLI_USAGE, dialsTheDaemon, } from './cli/cli-parse.js';
54
57
  import { handleFeedback, handleIdentify, handleTelemetry } from '../telemetry/feedback-cli.js';
55
58
  import { installDaemonTelemetry } from '../telemetry/daemon-telemetry.js';
56
59
  import { reportCliRun } from '../telemetry/cli-telemetry.js';
57
60
  // Re-exported so existing imports (and the CLI tests) keep resolving from '@/cli.js'.
58
61
  export { parseCliArgs, CLI_USAGE };
59
- function handleInit(parsed) {
62
+ async function handleInit(parsed) {
60
63
  const cwd = process.cwd();
61
- const io = buildNodeIo(cwd, serverInitHost());
64
+ // RETICLE_PORT counts as explicit, as it does for every other command: resolved once here, so the
65
+ // port written into the project and the port the runtime phase binds are the same number.
66
+ const explicit = parsed.port ?? portFromEnv(process.env);
67
+ const port = await portForInit(explicit, readProjectPort(cwd), readProjectId(cwd), {
68
+ // The registry names a daemon's owner; an older daemon that never registered is known by the
69
+ // projects its connected pages announced.
70
+ daemonProjects: async (p) => {
71
+ const claimed = daemonProjectAt(p, reticleStateHome());
72
+ if (claimed !== undefined && claimed.length > 0)
73
+ return [claimed];
74
+ const { sessions } = summarizeStatus(await fetchStatus(p));
75
+ return [
76
+ ...new Set(sessions.flatMap((s) => (s.projectId === undefined ? [] : [s.projectId]))),
77
+ ];
78
+ },
79
+ daemonPresent: async (p) => presenceIsUsable(await probePresence(p, { tcpOpen: probeDaemon, status: fetchStatus })),
80
+ pickPort: (p) => pickDaemonPortToBind(p),
81
+ });
82
+ const io = buildNodeIo(cwd, serverInitHost(), { stderr: true === parsed.json });
62
83
  const result = runInit({
63
84
  cwd,
64
- port: parsed.port,
85
+ port,
65
86
  mcp: parsed.mcp,
66
87
  dryRun: parsed.dryRun,
67
88
  install: parsed.install,
@@ -76,7 +97,7 @@ function handleInit(parsed) {
76
97
  deferOutcome: true,
77
98
  continuesToRuntime: true !== parsed.filesOnly && true !== parsed.dryRun,
78
99
  }, io);
79
- void continueAfterInit(parsed, result, io, cwd);
100
+ await continueAfterInit({ ...parsed, port }, result, io, cwd);
80
101
  }
81
102
  // `serve`, `stop` and `restart` live in `cli/lifecycle/daemon-lifecycle.ts`: one idea, and the
82
103
  // largest group this file could give up without splitting something that belongs together.
@@ -524,6 +545,26 @@ export function main() {
524
545
  if (licenseKey !== undefined)
525
546
  process.env[LICENSE_KEY_ENV] = licenseKey;
526
547
  const argv = process.argv.slice(2);
548
+ // Before anything reports or writes: a CLI a major away from the project's SDK makes every verdict
549
+ // `version_skew`, and the unpinned `npx @reticlehq/server` every agent entry uses resolves the
550
+ // LATEST major whatever the project installed. Hand the same arguments to the matching release and
551
+ // step aside — it reports its own run. See cli/launch/sdk-version-match.ts.
552
+ const matchVersion = versionToMatch({
553
+ argv,
554
+ cliVersion: SERVER_VERSION,
555
+ env: process.env,
556
+ projectDir: projectDirOf(process.cwd()),
557
+ readFile: readTextFile,
558
+ });
559
+ if (matchVersion !== undefined) {
560
+ process.stderr.write(`${versionMatchNote(matchVersion, SERVER_VERSION)}\n`);
561
+ reexecAtVersion(matchVersion, argv, process.env, {
562
+ spawn: nodeSpawner,
563
+ exit: (code) => process.exit(code),
564
+ warn: (line) => process.stderr.write(`${line}\n`),
565
+ });
566
+ return;
567
+ }
527
568
  // Every invocation passes through here — the single chokepoint for the "how often is it used / how
528
569
  // many distinct machines + projects" metrics. Fire-and-forget: a metric must never delay or fail a run.
529
570
  //
@@ -546,17 +587,39 @@ export function main() {
546
587
  process.stdout.write(`${CLI_USAGE}\n`);
547
588
  return;
548
589
  }
590
+ if ('connect' === argv[0]) {
591
+ void (async () => {
592
+ // A fresh project gets the same install, dev-server handover and browser proof as `init`.
593
+ // Its failure exits non-zero before any cloud binding can claim this repo is ready.
594
+ if (readProjectId(process.cwd()) === undefined) {
595
+ await handleInit({ port: undefined, mcp: true, dryRun: false, install: true });
596
+ }
597
+ return runCloudCommand(argv);
598
+ })()
599
+ .then((code) => process.exit(code))
600
+ .catch((cause) => {
601
+ process.stderr.write(`reticle connect: ${cause instanceof Error ? cause.message : String(cause)}\n`);
602
+ process.exit(1);
603
+ });
604
+ return;
605
+ }
549
606
  void runCloudCommand(argv).then((code) => process.exit(code));
550
607
  return;
551
608
  }
552
- const portEnv = process.env[ReticleEnv.PORT];
553
- const envPort = portEnv !== undefined ? parseInt(portEnv, 10) : undefined;
609
+ const envPort = portFromEnv(process.env);
554
610
  const projectPort = readProjectPort(process.cwd());
555
611
  // Say so rather than letting the daemon fight the dev server for the port and fail with an
556
612
  // EADDRINUSE that mentions neither file nor cause.
557
613
  if (projectPort !== undefined && isLikelyDevServerPort(projectPort)) {
558
614
  process.stderr.write(`${devServerPortWarning(projectPort)}\n`);
559
615
  }
616
+ // A monorepo root whose wired apps disagree on a port: nothing is picked, and without this line the
617
+ // default below would be — another project's daemon, answered as if it were this one.
618
+ const portConflict = projectPort === undefined && envPort === undefined && !argv.includes(PORT_FLAG)
619
+ ? workspacePortConflict(process.cwd())
620
+ : undefined;
621
+ if (portConflict !== undefined)
622
+ process.stderr.write(`${portConflict}\n`);
560
623
  // Registry BEFORE the default, and after both explicit sources.
561
624
  //
562
625
  // This is the line that ends the split brain. Build plugins have always asked the registry which
@@ -573,6 +636,11 @@ export function main() {
573
636
  // Headed by default; hidden only where there is no display to be headed on. A run nobody can see
574
637
  // is a run nobody trusts, and every "did it actually do anything?" cost a human round-trip.
575
638
  const parsed = parseCliArgs(argv, defaultPort, process.env['CI'] !== undefined);
639
+ // The refusal the line above promised. Printing it and carrying on let the default port answer
640
+ // anyway, for whichever project's daemon owned it.
641
+ if (portConflict !== undefined && dialsTheDaemon(parsed)) {
642
+ process.exit(1);
643
+ }
576
644
  switch (parsed.kind) {
577
645
  case 'error':
578
646
  // Two audiences, two channels. The structured line is for logs and scripts; the plain text is
@@ -583,7 +651,7 @@ export function main() {
583
651
  process.exit(1);
584
652
  break;
585
653
  case 'init':
586
- handleInit(parsed);
654
+ void handleInit(parsed);
587
655
  break;
588
656
  case 'serve':
589
657
  handleServe(parsed);
@@ -669,7 +737,7 @@ export function main() {
669
737
  void handleHunt(parsed.dir);
670
738
  break;
671
739
  case 'gate':
672
- void handleGate(parsed.files, parsed.since, parsed.hook);
740
+ void handleGate(parsed.files, parsed.since, parsed.hook, parsed.acceptCoverage);
673
741
  break;
674
742
  case 'report':
675
743
  void handleReport(parsed.session, parsed.hook);
@@ -1,5 +1,15 @@
1
- /** GET the daemon's /status JSON. Resolves to the parsed body, or undefined on any failure. */
2
- export declare function fetchStatus(port: number): Promise<unknown>;
1
+ /**
2
+ * GET the daemon's /status JSON. Resolves to the parsed body, or undefined on any failure —
3
+ * including a body that is not a Reticle daemon's, so every caller reads a stranger as absent and
4
+ * `probePresence` calls the port FOREIGN.
5
+ *
6
+ * `host` defaults to loopback, which is every caller's actual target — except `bootSession`
7
+ * (reticlehq/reticle#1165 review), whose Bridge can bind a non-default host via `RETICLE_HOST`.
8
+ * Hardcoding loopback here made that preflight check the wrong address.
9
+ */
10
+ export declare function fetchStatus(port: number, host?: string,
11
+ /** Also tell the daemon a setup run is waiting on it, so it does not idle out mid-wait. */
12
+ hold?: boolean): Promise<unknown>;
3
13
  /** One connected tab as `reticle status` reports it — the at-a-glance health line. */
4
14
  interface StatusSession {
5
15
  sessionId: string;
@@ -16,6 +26,8 @@ interface StatusSession {
16
26
  pendingMarks: number;
17
27
  /** The tab is attached and answering nothing — see Session.unresponsive. Absent on an older daemon. */
18
28
  unresponsive?: true;
29
+ /** Since the SDK last sent anything, heartbeats included. Absent on a body that did not say. */
30
+ lastSeenMs?: number;
19
31
  }
20
32
  /**
21
33
  * Reduce the daemon's /status JSON to the compact view `reticle status` prints. Pure: narrows the
@@ -1,5 +1,6 @@
1
1
  import * as http from 'node:http';
2
- import { LOOPBACK_HOST, STATUS_PATH } from '@reticlehq/core';
2
+ import { z } from 'zod';
3
+ import { LOOPBACK_HOST, STATUS_HOLD_QUERY, STATUS_PATH } from '@reticlehq/core';
3
4
  import { loopbackAgent } from '../../../surface/loopback-agent.js';
4
5
  /**
5
6
  * The impure half of `probePresence`: ask the port whether a Reticle daemon is answering.
@@ -15,13 +16,38 @@ import { loopbackAgent } from '../../../surface/loopback-agent.js';
15
16
  */
16
17
  /** How long the daemon /status probe waits before giving up — a local loopback call is near-instant. */
17
18
  const STATUS_PROBE_TIMEOUT_MS = 1000;
18
- /** GET the daemon's /status JSON. Resolves to the parsed body, or undefined on any failure. */
19
- export function fetchStatus(port) {
19
+ /**
20
+ * The fields every Reticle daemon's `/status` has carried, which no stranger's is likely to.
21
+ *
22
+ * Any JSON used to count. An unrelated MCP server on the port init chose answered `/status` with its
23
+ * own body, was adopted as the daemon, the page's bridge dial got WebSocket 404s, and init blamed the
24
+ * dev server. `contract` is NOT required: a daemon from before it existed is still Reticle, and has
25
+ * to be recognised as one so init can replace it as skewed rather than call it a stranger.
26
+ */
27
+ const ReticleStatusSchema = z
28
+ .object({
29
+ running: z.literal(true),
30
+ version: z.string().min(1),
31
+ sessions: z.array(z.unknown()),
32
+ })
33
+ .passthrough();
34
+ /**
35
+ * GET the daemon's /status JSON. Resolves to the parsed body, or undefined on any failure —
36
+ * including a body that is not a Reticle daemon's, so every caller reads a stranger as absent and
37
+ * `probePresence` calls the port FOREIGN.
38
+ *
39
+ * `host` defaults to loopback, which is every caller's actual target — except `bootSession`
40
+ * (reticlehq/reticle#1165 review), whose Bridge can bind a non-default host via `RETICLE_HOST`.
41
+ * Hardcoding loopback here made that preflight check the wrong address.
42
+ */
43
+ export function fetchStatus(port, host = LOOPBACK_HOST,
44
+ /** Also tell the daemon a setup run is waiting on it, so it does not idle out mid-wait. */
45
+ hold = false) {
20
46
  return new Promise((resolve) => {
21
47
  const req = http.get({
22
- host: LOOPBACK_HOST,
48
+ host,
23
49
  port,
24
- path: STATUS_PATH,
50
+ path: hold ? `${STATUS_PATH}?${STATUS_HOLD_QUERY}=1` : STATUS_PATH,
25
51
  timeout: STATUS_PROBE_TIMEOUT_MS,
26
52
  // Shares the proxy's keep-alive agent: the proxy calls this on every reconnect, and a socket
27
53
  // per probe is the same churn the POST leg was fixed for.
@@ -32,7 +58,8 @@ export function fetchStatus(port) {
32
58
  res.on('data', (chunk) => (body += chunk));
33
59
  res.on('end', () => {
34
60
  try {
35
- resolve(JSON.parse(body));
61
+ const parsed = ReticleStatusSchema.safeParse(JSON.parse(body));
62
+ resolve(parsed.success ? parsed.data : undefined);
36
63
  }
37
64
  catch {
38
65
  resolve(undefined);
@@ -80,6 +107,7 @@ export function summarizeStatus(payload) {
80
107
  stale: true === r['stale'],
81
108
  pendingMarks: 'number' === typeof r['pendingMarks'] ? r['pendingMarks'] : 0,
82
109
  ...(true === r['unresponsive'] ? { unresponsive: true } : {}),
110
+ ...('number' === typeof r['lastSeenMs'] ? { lastSeenMs: r['lastSeenMs'] } : {}),
83
111
  };
84
112
  })
85
113
  .filter((s) => s !== null);
@@ -16,6 +16,15 @@
16
16
  * This module is the one answer both sides now use. It is pure and injectable: no process spawning,
17
17
  * no port binding, nothing that needs a real `~/.reticle` to test.
18
18
  */
19
+ import { type DaemonRegistryEntry } from '@reticlehq/core/artifacts';
20
+ /**
21
+ * Every well-formed registry entry in `home`.
22
+ *
23
+ * A corrupt or half-written entry is skipped rather than thrown on: the registry is written by other
24
+ * processes that can be killed mid-write, and one bad file must not make a machine's daemons
25
+ * undiscoverable. Validated through core's schema so a shape change cannot silently pass here.
26
+ */
27
+ export declare function readDaemonRegistry(home: string): DaemonRegistryEntry[];
19
28
  /**
20
29
  * The port of the live daemon serving `projectId`, or undefined when this project has none.
21
30
  *
@@ -26,7 +26,7 @@ import { DaemonRegistryEntrySchema, daemonRegistryPort, pickDaemonPort, } from '
26
26
  * processes that can be killed mid-write, and one bad file must not make a machine's daemons
27
27
  * undiscoverable. Validated through core's schema so a shape change cannot silently pass here.
28
28
  */
29
- function readDaemonRegistry(home) {
29
+ export function readDaemonRegistry(home) {
30
30
  let files;
31
31
  try {
32
32
  files = readdirSync(home);
@@ -21,6 +21,12 @@
21
21
  */
22
22
  export declare function noteToolCall(): void;
23
23
  export declare function everServedToolCall(): boolean;
24
+ /**
25
+ * How long one hold from a setup run keeps the daemon from idling out. `init` renews it on a timer
26
+ * well inside this window for as long as it runs, so a run that dies stops holding within it.
27
+ */
28
+ export declare const SETUP_HOLD_MS = 60000;
29
+ export declare function noteSetupHold(now: number): void;
24
30
  /** Tests only. */
25
31
  export declare function resetDaemonUsefulness(): void;
26
32
  interface UsefulnessFacts {
@@ -56,5 +62,5 @@ interface IdlePool {
56
62
  * clause the shutdown watcher could never fire on the many daemons that spawn in a directory with
57
63
  * no app and sit there for the length of an editor session.
58
64
  */
59
- export declare function buildIdlePredicate(agentConnected: () => boolean, sessions: IdleSessions, pool: IdlePool): () => boolean;
65
+ export declare function buildIdlePredicate(agentConnected: () => boolean, sessions: IdleSessions, pool: IdlePool, now?: () => number): () => boolean;
60
66
  export {};