@reticlehq/server 2.8.0 → 2.10.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 (318) hide show
  1. package/SKILL.md +130 -813
  2. package/dist/bridge/bridge.d.ts +23 -0
  3. package/dist/bridge/bridge.js +80 -2
  4. package/dist/bridge/bridge.test-harness.d.ts +5 -0
  5. package/dist/bridge/bridge.test-harness.js +6 -4
  6. package/dist/capsule/capsule-store.d.ts +2 -2
  7. package/dist/capsule/causal-summary.d.ts +16 -1
  8. package/dist/capsule/causal-summary.js +2 -1
  9. package/dist/cli/cli-doctor.js +41 -13
  10. package/dist/cli/cli-flow-commands.js +8 -8
  11. package/dist/cli/cli-launch.d.ts +0 -17
  12. package/dist/cli/cli-launch.js +0 -28
  13. package/dist/cli/cli-parse.d.ts +1 -1
  14. package/dist/cli/cli-parse.js +1 -1
  15. package/dist/cli/cli-port.d.ts +24 -0
  16. package/dist/cli/cli-port.js +66 -35
  17. package/dist/cli/cli-verify.d.ts +21 -0
  18. package/dist/cli/cli-verify.js +45 -1
  19. package/dist/cli/config-discovery.d.ts +35 -0
  20. package/dist/cli/config-discovery.js +163 -0
  21. package/dist/cli/doctor-daemon-line.js +4 -1
  22. package/dist/cli/doctor-project-line.d.ts +37 -0
  23. package/dist/cli/doctor-project-line.js +35 -0
  24. package/dist/cli/doctor-rows.d.ts +51 -0
  25. package/dist/cli/doctor-rows.js +54 -0
  26. package/dist/cli/doctor-sessions-line.js +6 -3
  27. package/dist/cli/drive-attach.d.ts +99 -0
  28. package/dist/cli/drive-attach.js +203 -0
  29. package/dist/cli/drive-command.d.ts +13 -0
  30. package/dist/cli/drive-command.js +103 -0
  31. package/dist/cli/open-note.d.ts +20 -0
  32. package/dist/cli/open-note.js +37 -0
  33. package/dist/cli/status-next-action.d.ts +37 -0
  34. package/dist/cli/status-next-action.js +61 -0
  35. package/dist/cli.js +65 -70
  36. package/dist/crawl/crawl.d.ts +7 -2
  37. package/dist/crawl/crawl.js +52 -9
  38. package/dist/ee/audit-log.js +2 -2
  39. package/dist/events/body-failures.d.ts +2 -2
  40. package/dist/events/contradiction-folds.d.ts +45 -0
  41. package/dist/events/contradiction-folds.js +42 -0
  42. package/dist/events/contradictions.d.ts +51 -1
  43. package/dist/events/contradictions.js +26 -7
  44. package/dist/events/declared.d.ts +45 -0
  45. package/dist/events/declared.js +73 -0
  46. package/dist/events/echo-mismatch.d.ts +9 -3
  47. package/dist/events/echo-mismatch.js +33 -5
  48. package/dist/events/event-filters.d.ts +1 -1
  49. package/dist/events/event-filters.js +7 -4
  50. package/dist/events/predicate-eval.d.ts +17 -91
  51. package/dist/events/predicate-eval.js +85 -326
  52. package/dist/events/predicate-parse.js +31 -3
  53. package/dist/events/predicate-route.d.ts +32 -0
  54. package/dist/events/predicate-route.js +102 -0
  55. package/dist/events/predicate-schema.d.ts +99 -0
  56. package/dist/events/predicate-schema.js +309 -0
  57. package/dist/events/predicate.d.ts +8 -0
  58. package/dist/events/predicate.js +127 -5
  59. package/dist/events/stale-response.d.ts +2 -3
  60. package/dist/events/stale-response.js +49 -2
  61. package/dist/events/unit-mismatch.d.ts +2 -2
  62. package/dist/flows/annotate-tools.js +3 -0
  63. package/dist/flows/annotate.js +18 -4
  64. package/dist/flows/coverage.d.ts +38 -5
  65. package/dist/flows/coverage.js +30 -4
  66. package/dist/flows/decision.d.ts +15 -0
  67. package/dist/flows/decision.js +1 -1
  68. package/dist/flows/flow-replay-run.js +8 -1
  69. package/dist/flows/flow-tools.js +9 -1
  70. package/dist/flows/replay.js +3 -0
  71. package/dist/flows/verify-change-tools.js +1 -1
  72. package/dist/honesty/accepted-write.d.ts +3 -0
  73. package/dist/honesty/accepted-write.js +8 -3
  74. package/dist/honesty/already-true.js +15 -0
  75. package/dist/honesty/blind-spots.js +4 -0
  76. package/dist/honesty/honesty.d.ts +11 -0
  77. package/dist/honesty/honesty.js +1 -0
  78. package/dist/honesty/uncaptured-bodies.d.ts +4 -2
  79. package/dist/honesty/uncaptured-bodies.js +9 -2
  80. package/dist/honesty/unread-outcome.d.ts +8 -0
  81. package/dist/honesty/unread-outcome.js +36 -9
  82. package/dist/honesty/unsettled.d.ts +9 -0
  83. package/dist/honesty/unsettled.js +7 -3
  84. package/dist/honesty/verified.d.ts +29 -3
  85. package/dist/honesty/verified.js +67 -8
  86. package/dist/http-server.d.ts +19 -0
  87. package/dist/http-server.js +202 -4
  88. package/dist/impact/impact-recorder.d.ts +23 -0
  89. package/dist/impact/impact-recorder.js +76 -0
  90. package/dist/impact/impact-store.d.ts +47 -0
  91. package/dist/impact/impact-store.js +171 -0
  92. package/dist/index.d.ts +25 -0
  93. package/dist/index.js +60 -10
  94. package/dist/init/agent-rules.d.ts +20 -0
  95. package/dist/init/agent-rules.js +36 -0
  96. package/dist/init/astro-patch.js +27 -1
  97. package/dist/init/confirm.d.ts +21 -3
  98. package/dist/init/confirm.js +32 -5
  99. package/dist/init/connect-steps.d.ts +2 -0
  100. package/dist/init/connect-steps.js +50 -0
  101. package/dist/init/csp-check.d.ts +38 -0
  102. package/dist/init/csp-check.js +92 -0
  103. package/dist/init/csp-doctor.d.ts +25 -0
  104. package/dist/init/csp-doctor.js +49 -0
  105. package/dist/init/cursor.js +6 -9
  106. package/dist/init/detect.d.ts +8 -0
  107. package/dist/init/detect.js +33 -3
  108. package/dist/init/mcp-clients.d.ts +1 -0
  109. package/dist/init/mcp-clients.js +51 -7
  110. package/dist/init/mcp.d.ts +16 -2
  111. package/dist/init/mcp.js +23 -1
  112. package/dist/init/plan-framework.d.ts +10 -0
  113. package/dist/init/plan-framework.js +25 -1
  114. package/dist/init/plan.d.ts +7 -2
  115. package/dist/init/plan.js +66 -30
  116. package/dist/init/run.js +11 -5
  117. package/dist/init/slash-command.d.ts +1 -1
  118. package/dist/init/slash-command.js +12 -4
  119. package/dist/init/snippets.d.ts +1 -1
  120. package/dist/init/snippets.js +35 -7
  121. package/dist/init/vite-config.js +24 -2
  122. package/dist/input/network-mock-tools.js +8 -1
  123. package/dist/input/scroll-find.d.ts +6 -0
  124. package/dist/input/scroll-find.js +17 -2
  125. package/dist/input/scroll-tools.js +4 -0
  126. package/dist/journal/attach-journal.d.ts +9 -1
  127. package/dist/journal/attach-journal.js +13 -2
  128. package/dist/journal/journal-query.d.ts +13 -0
  129. package/dist/journal/journal-query.js +28 -3
  130. package/dist/journal/workspace-gitignore.d.ts +10 -0
  131. package/dist/journal/workspace-gitignore.js +59 -0
  132. package/dist/license/license.d.ts +69 -4
  133. package/dist/license/license.js +84 -15
  134. package/dist/mcp/mcp-proxy.d.ts +7 -93
  135. package/dist/mcp/mcp-proxy.js +183 -250
  136. package/dist/mcp/mcp.d.ts +27 -2
  137. package/dist/mcp/mcp.js +77 -36
  138. package/dist/mcp/proxy-daemon-probe.d.ts +8 -0
  139. package/dist/mcp/proxy-daemon-probe.js +48 -0
  140. package/dist/mcp/proxy-handshake.d.ts +82 -1
  141. package/dist/mcp/proxy-handshake.js +110 -3
  142. package/dist/mcp/proxy-lifecycle.d.ts +10 -1
  143. package/dist/mcp/proxy-lifecycle.js +11 -2
  144. package/dist/mcp/proxy-log.d.ts +63 -0
  145. package/dist/mcp/proxy-log.js +132 -0
  146. package/dist/mcp/server-instructions.d.ts +17 -0
  147. package/dist/mcp/server-instructions.js +56 -0
  148. package/dist/mcp/shared-params.d.ts +27 -0
  149. package/dist/mcp/shared-params.js +36 -0
  150. package/dist/mcp/sse-frame-parser.d.ts +19 -0
  151. package/dist/mcp/sse-frame-parser.js +41 -0
  152. package/dist/project/reticle-dir.d.ts +2 -0
  153. package/dist/project/reticle-dir.js +1 -0
  154. package/dist/session/connection-memory.d.ts +59 -0
  155. package/dist/session/connection-memory.js +108 -0
  156. package/dist/session/dev-command.d.ts +26 -0
  157. package/dist/session/dev-command.js +89 -0
  158. package/dist/session/dev-server-probe.d.ts +68 -0
  159. package/dist/session/dev-server-probe.js +102 -7
  160. package/dist/session/last-act.d.ts +26 -0
  161. package/dist/session/last-act.js +35 -1
  162. package/dist/session/live-control-tools.js +42 -5
  163. package/dist/session/no-session-diagnosis.d.ts +55 -2
  164. package/dist/session/no-session-diagnosis.js +199 -22
  165. package/dist/session/no-session-next-action.d.ts +46 -0
  166. package/dist/session/no-session-next-action.js +85 -0
  167. package/dist/session/no-session-watch.d.ts +40 -1
  168. package/dist/session/no-session-watch.js +172 -11
  169. package/dist/session/pending-commands.d.ts +12 -0
  170. package/dist/session/pending-commands.js +13 -1
  171. package/dist/session/session-manager.d.ts +4 -0
  172. package/dist/session/session-manager.js +35 -5
  173. package/dist/session/session-readiness.js +1 -1
  174. package/dist/session/session.d.ts +10 -1
  175. package/dist/session/session.js +146 -5
  176. package/dist/telemetry/app-instrumented.d.ts +8 -0
  177. package/dist/telemetry/app-instrumented.js +10 -0
  178. package/dist/telemetry/bug-found.d.ts +18 -0
  179. package/dist/telemetry/bug-found.js +14 -0
  180. package/dist/telemetry/daemon-telemetry.js +29 -0
  181. package/dist/telemetry/install-source.d.ts +20 -0
  182. package/dist/telemetry/install-source.js +34 -0
  183. package/dist/telemetry/instrumentation-stall.d.ts +57 -0
  184. package/dist/telemetry/instrumentation-stall.js +103 -0
  185. package/dist/telemetry/license-activation.d.ts +48 -0
  186. package/dist/telemetry/license-activation.js +57 -0
  187. package/dist/telemetry/session-metrics.d.ts +8 -0
  188. package/dist/telemetry/session-metrics.js +10 -0
  189. package/dist/telemetry/telemetry.d.ts +3 -1
  190. package/dist/telemetry/telemetry.js +23 -3
  191. package/dist/tools/act-tools.js +166 -29
  192. package/dist/tools/assert-verdict.js +32 -7
  193. package/dist/tools/browser-tools.js +10 -0
  194. package/dist/tools/dynamic-tools.js +17 -8
  195. package/dist/tools/error-recovery.js +10 -6
  196. package/dist/tools/feedback-tools.js +31 -6
  197. package/dist/tools/invoke-tool.d.ts +1 -1
  198. package/dist/tools/invoke-tool.js +30 -8
  199. package/dist/tools/lease-hint.d.ts +30 -8
  200. package/dist/tools/lease-hint.js +65 -14
  201. package/dist/tools/lease-tools.d.ts +11 -0
  202. package/dist/tools/lease-tools.js +141 -96
  203. package/dist/tools/observe-tools.js +9 -3
  204. package/dist/tools/react-grace.d.ts +4 -0
  205. package/dist/tools/react-grace.js +6 -3
  206. package/dist/tools/read-tools.js +47 -1
  207. package/dist/tools/resolve-target.d.ts +33 -0
  208. package/dist/tools/resolve-target.js +53 -0
  209. package/dist/tools/settle-in-flight.d.ts +15 -0
  210. package/dist/tools/settle-in-flight.js +42 -2
  211. package/dist/tools/snapshot-delta.d.ts +3 -0
  212. package/dist/tools/snapshot-delta.js +67 -6
  213. package/dist/tools/tool-kit.d.ts +38 -3
  214. package/dist/tools/tool-names.d.ts +2 -0
  215. package/dist/tools/tool-names.js +2 -0
  216. package/dist/tools/tool-surface.d.ts +60 -0
  217. package/dist/tools/tool-surface.js +84 -19
  218. package/dist/tools/tools.js +61 -5
  219. package/dist/tools/unadvertised-help.js +9 -6
  220. package/docs/README.md +1 -2
  221. package/docs/actions.mdx +41 -8
  222. package/docs/agent-cheatsheet.md +6 -4
  223. package/docs/architecture.md +6 -0
  224. package/docs/benchmarks.md +60 -12
  225. package/docs/best-practices.mdx +11 -6
  226. package/docs/capabilities.mdx +204 -57
  227. package/docs/cli/affected.mdx +6 -4
  228. package/docs/cli/capsules.mdx +9 -8
  229. package/docs/cli/cloud.mdx +30 -4
  230. package/docs/cli/doctor.mdx +41 -8
  231. package/docs/cli/drive.mdx +34 -15
  232. package/docs/cli/gate.mdx +6 -6
  233. package/docs/cli/help.mdx +8 -6
  234. package/docs/cli/hunt.mdx +20 -4
  235. package/docs/cli/identify.mdx +16 -2
  236. package/docs/cli/init.mdx +35 -8
  237. package/docs/cli/kill.mdx +5 -5
  238. package/docs/cli/license.mdx +1 -1
  239. package/docs/cli/open.mdx +6 -0
  240. package/docs/cli/restart.mdx +6 -6
  241. package/docs/cli/serve.mdx +10 -3
  242. package/docs/cli/status.mdx +29 -5
  243. package/docs/cli/stop.mdx +8 -0
  244. package/docs/cli/verify.mdx +20 -1
  245. package/docs/cli/version.mdx +1 -1
  246. package/docs/cli/watch.mdx +9 -2
  247. package/docs/cli.mdx +44 -22
  248. package/docs/debugging.md +3 -1
  249. package/docs/deploy-checks.md +31 -2
  250. package/docs/desktop.mdx +276 -80
  251. package/docs/docs.json +13 -12
  252. package/docs/enterprise.md +9 -3
  253. package/docs/faq.mdx +49 -35
  254. package/docs/fixtures.md +2 -0
  255. package/docs/flows.md +7 -5
  256. package/docs/for-agents.md +3 -3
  257. package/docs/frameworks.mdx +46 -7
  258. package/docs/gate-plan.md +4 -2
  259. package/docs/gates.md +8 -6
  260. package/docs/getting-started.md +53 -12
  261. package/docs/human-control.md +2 -0
  262. package/docs/index.mdx +43 -1
  263. package/docs/install-agentic.mdx +13 -4
  264. package/docs/install-manual.mdx +6 -4
  265. package/docs/instrumentation.mdx +7 -1
  266. package/docs/integration-patterns.md +3 -1
  267. package/docs/local-registry.md +4 -2
  268. package/docs/multi-agent-testing.md +3 -1
  269. package/docs/packages/babel-plugin.mdx +15 -6
  270. package/docs/packages/browser.mdx +2 -2
  271. package/docs/packages/core.mdx +26 -16
  272. package/docs/packages/electron.mdx +5 -7
  273. package/docs/packages/eslint-plugin.mdx +5 -3
  274. package/docs/packages/next.mdx +3 -5
  275. package/docs/packages/react.mdx +3 -3
  276. package/docs/packages/server.mdx +2 -2
  277. package/docs/packages/tauri.mdx +6 -6
  278. package/docs/packages/test.mdx +18 -7
  279. package/docs/packages/vite-plugin.mdx +4 -6
  280. package/docs/packages.mdx +14 -13
  281. package/docs/platform-integration.md +2 -0
  282. package/docs/predicates.mdx +76 -7
  283. package/docs/quickstart.mdx +51 -5
  284. package/docs/recipes.mdx +63 -23
  285. package/docs/skill-file.mdx +3 -1
  286. package/docs/state-management.mdx +2 -0
  287. package/docs/system-map.md +3 -1
  288. package/docs/telemetry-contract.md +33 -5
  289. package/docs/telemetry-events.mdx +63 -18
  290. package/docs/telemetry.md +8 -5
  291. package/docs/testing.md +35 -1
  292. package/docs/token-efficiency.md +8 -2
  293. package/docs/tools-act-and-wait.mdx +64 -14
  294. package/docs/tools-act-sequence.mdx +34 -17
  295. package/docs/tools-act.mdx +24 -11
  296. package/docs/tools-assert.mdx +69 -10
  297. package/docs/tools-console.mdx +11 -7
  298. package/docs/tools-inspect.mdx +11 -8
  299. package/docs/tools-navigate.mdx +41 -5
  300. package/docs/tools-network.mdx +42 -9
  301. package/docs/tools-observe.mdx +66 -10
  302. package/docs/tools-overview.mdx +26 -23
  303. package/docs/tools-query.mdx +52 -8
  304. package/docs/tools-session-and-feedback.mdx +35 -9
  305. package/docs/tools-sessions.mdx +39 -10
  306. package/docs/tools-snapshot.mdx +57 -3
  307. package/docs/tools-state.mdx +24 -1
  308. package/docs/tools-tools-and-run.mdx +55 -24
  309. package/docs/tools-wait-for.mdx +34 -15
  310. package/docs/troubleshooting.mdx +80 -10
  311. package/docs/usage.md +46 -24
  312. package/docs/vs-chrome-devtools-mcp.mdx +41 -0
  313. package/docs/vs-playwright-mcp.mdx +42 -2
  314. package/docs/vs-screenshots.mdx +42 -1
  315. package/docs/what-is-reticle.mdx +1 -1
  316. package/docs/why-reticle.mdx +3 -1
  317. package/package.json +5 -5
  318. package/docs/desktop-apps.md +0 -265
@@ -19,6 +19,9 @@
19
19
  * the hot resolve() path and this stays unit-testable.
20
20
  */
21
21
  import { DEV_SERVER_PORTS } from '../cli/cli-port.js';
22
+ import { STALL_AFTER_MS } from '../telemetry/instrumentation-stall.js';
23
+ /** The one framework whose most likely cause differs from every other framework's. */
24
+ const NUXT = 'nuxt';
22
25
  /**
23
26
  * Every branch ends with this, and `recoveryFor` keys on it to suppress the generic no-session
24
27
  * recovery: a message that already carries its own next action must not be handed a second, more
@@ -40,6 +43,21 @@ const RETRY = SELF_RECOVERING_MARKER;
40
43
  * Only offered when the app is known to carry the SDK: leasing an uninstrumented app just burns a
41
44
  * browser and comes back `ready:false`.
42
45
  */
46
+ /**
47
+ * The no-shell path, for the branches where a BLIND lease would be wrong.
48
+ *
49
+ * A lease opens a URL, so offering one while nothing is listening is offering to open nothing —
50
+ * which is why these branches deliberately withheld it, and a test pins that. But withholding it
51
+ * left an agent with no CLI holding no path at all: reported from Windows, where the MCP server was
52
+ * registered and every tool advertised while no `reticle` binary existed on disk, so every remedy
53
+ * offered was a shell command that could not be run.
54
+ *
55
+ * The resolution is the ORDER, not the offer. Get the URL first — which these branches already tell
56
+ * the reader to do — and then the lease is the way to open it without a shell.
57
+ */
58
+ const URL_THEN_LEASE = 'Once you have that URL, open it with reticle_lease {action:"acquire", url} rather than a shell ' +
59
+ 'command — it needs no CLI on PATH (reach it with reticle_run {tool:"reticle_lease"} if it is ' +
60
+ 'not advertised directly).';
43
61
  const SELF_SERVE = 'You do not have to wait for the human: reticle_lease {action:"acquire", url} opens a browser ' +
44
62
  'Reticle drives itself, and returns a sessionId you can use immediately (reach it with ' +
45
63
  'reticle_run {tool:"reticle_lease"} if it is not advertised directly; release it when you finish).';
@@ -66,9 +84,82 @@ const SELF_SERVE = 'You do not have to wait for the human: reticle_lease {action
66
84
  */
67
85
  const NON_LOCALHOST_GATE = 'One more cause that leaves every other check healthy: if the app is served on anything other ' +
68
86
  'than localhost (a hosts-file alias, a LAN IP, a tunnel), the SDK refuses to connect unless it ' +
69
- 'is given `allowNonLocalhost: true` — the refusal is page-side, so nothing here can see it. ' +
70
- "Check the browser console for that message and pass the flag in the app's reticle.connect().";
87
+ 'is given `allowNonLocalhost: true` AND a pairing token — the flag alone is NOT sufficient off ' +
88
+ 'localhost, which cost one reporter their whole setup until they read our compiled SDK by hand. ' +
89
+ 'The token is the one in `~/.reticle/pairing-token`, passed as `token` to the same connect(). ' +
90
+ 'The refusal is page-side, so nothing here can see it: check the browser console for that ' +
91
+ "message and pass both in the app's reticle.connect().";
92
+ /**
93
+ * The causes that actually occurred in the field, for an app that is wired and still silent.
94
+ *
95
+ * Every one of these is indistinguishable from "no SDK installed" from every surface an agent can
96
+ * reach, and the old message modelled none of them — it modelled a missing SDK and a wrong port, and
97
+ * across a batch of reports on four apps the port was right every time. An agent that is told the
98
+ * wrong differential does not merely waste calls: several of these reports end with the agent
99
+ * telling its human that Reticle was not set up, on an app that was correctly wired.
100
+ */
101
+ const REAL_CAUSES = 'Causes that produce exactly this and are NOT a port problem, in the order they actually occur: ' +
102
+ '(a) the SDK is in the bundle but `connect()` is never reached — a dev-mode guard or a missing ' +
103
+ 'runtime-config value returning early, which emits nothing at all, so grep the app for the ' +
104
+ 'connect call and confirm it runs; (b) the dev server was started BEFORE the Reticle plugin was ' +
105
+ 'added, so it is not in the bundle at all — restart the dev server, do not rely on HMR; ' +
106
+ '(c) a peer dependency is missing, typically `@reticlehq/react` in a non-React app, so the ' +
107
+ "dynamic import fails silently; (d) this daemon's working directory is not the project, so it is " +
108
+ 'scoped to the wrong app entirely — check the directory named above is really where your app ' +
109
+ 'lives.';
110
+ /** Nuxt's most likely cause is (b), and Nuxt is the framework where it is nearly certain. */
111
+ const NUXT_STALE_BUNDLE = 'This project is Nuxt: the most likely cause by a distance is a dev server that predates the ' +
112
+ 'Reticle plugin. Nuxt does NOT register a newly added plugin on HMR, so the running bundle ' +
113
+ 'carries no SDK however correct the config is — STOP and restart `nuxt dev`, then reload the ' +
114
+ 'page, before investigating anything else.';
115
+ /**
116
+ * The lead when durable state proves this project has connected on this port before.
117
+ *
118
+ * The daemon used to contradict itself inside one response: `reticle_sessions` said a session HAD
119
+ * connected earlier "so the wiring is correct", and a lease seconds later said the usual cause was a
120
+ * port mismatch. It already held the evidence against its own hint.
121
+ */
122
+ const RESTARTED_LEAD = 'no browser session connected. This daemon has served none since it started, but it is a NEW ' +
123
+ 'process — an app for this project HAS connected on this port before, which is recorded ' +
124
+ 'durably. So the wiring is correct and the port is correct: what is failing now is the SDK ' +
125
+ 'reaching initialise on the page. Re-running the install is the wrong move here: it cannot help ' +
126
+ 'on a project that has demonstrably connected, and it can overwrite a working config.';
71
127
  const SCANNED_PORTS = [...DEV_SERVER_PORTS].join(', ');
128
+ /**
129
+ * How to say "open the app" to an agent that may have no `reticle` binary.
130
+ *
131
+ * These strings are read by an AGENT that is already blocked, and they used to name a bare
132
+ * `reticle open <url>`. Reticle registers its MCP server as `npx @reticlehq/server mcp`, so the
133
+ * ordinary install puts NOTHING on PATH — the binary those messages assume is missing on most
134
+ * machines that ever read them.
135
+ *
136
+ * Reported from Windows, where a half-failed plugin install left the MCP server registered and all
137
+ * the tools advertised while no CLI existed on disk. The agent followed the remediation, found no
138
+ * `reticle`, tried `npx @reticlehq/reticle` (a package that does not exist and 404s), and had no
139
+ * path forward at all. The remedy has to be a command that works from a bare npm environment.
140
+ *
141
+ * Init-time messages keep the short form on purpose: there, the reader is already running the CLI.
142
+ */
143
+ const OPEN_CMD = '`npx @reticlehq/server open <url>`';
144
+ const OPEN_CMD_BARE = '`npx @reticlehq/server open`';
145
+ const INIT_CMD = '`npx @reticlehq/server init`';
146
+ /**
147
+ * The one sentence that stops the scan lying about a server that is running.
148
+ *
149
+ * A port that accepted a connection and then said nothing is evidence FOR the app being up, not
150
+ * against it, and it has to be read before the "nothing is listening" sentence rather than after —
151
+ * the reader acts on the first claim.
152
+ */
153
+ function slowListenerClause(facts) {
154
+ const slow = facts.slowListeners ?? [];
155
+ if (0 === slow.length)
156
+ return '';
157
+ const ports = slow.join(', ');
158
+ const subject = 1 === slow.length ? `Port ${ports} ACCEPTED` : `Ports ${ports} ACCEPTED`;
159
+ return (`${subject} a connection but did not answer in time, which is what a server-rendered dev ` +
160
+ 'server compiling its first route looks like — so something IS running there and you should ' +
161
+ 'open it rather than start it. Setting that aside: ');
162
+ }
72
163
  /**
73
164
  * The commonest first-run state there is, and until #320 the message named it nowhere.
74
165
  *
@@ -78,7 +169,7 @@ const SCANNED_PORTS = [...DEV_SERVER_PORTS].join(', ');
78
169
  * discovering that one `reticle open` fixed it instantly.
79
170
  */
80
171
  const OPEN_THE_APP = 'Reticle only ever sees a page that is LOADED, so the commonest cause by a distance is that no ' +
81
- "browser has opened the app yet: run `reticle open <url>` with the app's own URL (or ask the " +
172
+ `browser has opened the app yet: run ${OPEN_CMD} with the app's own URL (or ask the ` +
82
173
  'human to open it).';
83
174
  /**
84
175
  * What a machine-wide port scan can and cannot say.
@@ -114,6 +205,70 @@ function portMismatchClause(facts) {
114
205
  ` Either restart the daemon on ${String(configured)}, or update \`.reticle.json\` to` +
115
206
  ` ${String(facts.port)} and restart the dev server so the app picks it up.`);
116
207
  }
208
+ /**
209
+ * The tail every "the app is wired and silent" branch ends with, ranked.
210
+ *
211
+ * One place, because the ranking is the whole fix: a hint that lists "no server", "no SDK" and
212
+ * "no tab" at equal weight is a hint the reader has to rank itself, with strictly less evidence
213
+ * than the daemon has.
214
+ */
215
+ function rankedCauses(facts) {
216
+ const nuxt = facts.framework?.toLowerCase() === NUXT ? `${NUXT_STALE_BUNDLE} ` : '';
217
+ return (`${nuxt}${REAL_CAUSES} If none of those, the app may be dialling a different daemon than this ` +
218
+ `one (on ${String(facts.port)}): check the app's reticle port matches ` +
219
+ `${String(facts.port)}.${portMismatchClause(facts)} ${NON_LOCALHOST_GATE}`);
220
+ }
221
+ /**
222
+ * The scope answer, when the config was somewhere else all along.
223
+ *
224
+ * This OUTRANKS every "you may have no SDK" sentence in this file, because it is positive evidence:
225
+ * a `.reticle.json` in `apps/web` means that app has been through `init` whatever the daemon's own
226
+ * directory looks like. Naming the directories rather than adopting one is deliberate — several
227
+ * configs means several projects, and choosing silently is how an agent gets a confident verdict
228
+ * about an app it never touched.
229
+ */
230
+ function configsElsewhereClause(facts) {
231
+ const configs = facts.configsElsewhere ?? [];
232
+ if (0 === configs.length)
233
+ return '';
234
+ const named = configs
235
+ .map((c) => (c.projectId === undefined ? c.directory : `${c.directory} ('${c.projectId}')`))
236
+ .join(', ');
237
+ return (`A \`.reticle.json\` WAS found outside this daemon's directory: ${named}. So the app is wired ` +
238
+ 'and this is a SCOPE problem, not an install problem — this daemon was started somewhere that ' +
239
+ 'is not the project, which is the normal outcome when an editor launches it from your home ' +
240
+ "directory. Restart the daemon from the app's directory (or point it there) rather than " +
241
+ 'installing anything.');
242
+ }
243
+ /** Where we looked, when we found nothing. An absence nobody can check is not evidence. */
244
+ function searchedClause(facts) {
245
+ const searched = facts.searchedDirectories ?? [];
246
+ if (0 === searched.length)
247
+ return '';
248
+ if ((facts.configsElsewhere ?? []).length > 0)
249
+ return '';
250
+ return ` I looked in: ${searched.join(', ')}. If your app is not in that list, that is the answer — this daemon is standing somewhere else.`;
251
+ }
252
+ /**
253
+ * A leading sentence when the daemon has been up long enough that no app is coming.
254
+ *
255
+ * Only fires for a WIRED project (initialized) that has NEVER connected and is past the
256
+ * threshold. An unwired project already names the install gap as its primary cause; adding a time
257
+ * sentence there would double-blame.
258
+ */
259
+ function stallClause(facts) {
260
+ const { daemonUpMs, initialized, everConnected } = facts;
261
+ if (everConnected)
262
+ return '';
263
+ if (!initialized)
264
+ return '';
265
+ if (daemonUpMs === undefined || daemonUpMs < STALL_AFTER_MS)
266
+ return '';
267
+ const minutes = Math.round(daemonUpMs / 60_000);
268
+ return (`This daemon has been running for ${String(minutes)} minutes and no app has connected. ` +
269
+ 'The most likely cause is that the dev server was not restarted after adding the Reticle ' +
270
+ 'plugin, so the running bundle carries no SDK. Restart the dev server first. ');
271
+ }
117
272
  export function diagnoseNoSession(facts) {
118
273
  const { everConnected, initialized, listening, port } = facts;
119
274
  // Named when known: a claim about a missing file is a claim about ONE directory.
@@ -133,11 +288,30 @@ export function diagnoseNoSession(facts) {
133
288
  'that a lease you were using aged out; a lease is a headless context, not a human tab, and ' +
134
289
  'it takes its cookies with it (so an authenticated app needs signing in again). Re-acquire ' +
135
290
  'with reticle_lease {action:"acquire", url} and carry on. If you were driving a human tab ' +
136
- `instead, it went away — reopen it or run \`reticle open\`. ${RETRY}`);
291
+ `instead, it went away — reopen it or run ${OPEN_CMD_BARE}. ${RETRY}`);
137
292
  }
138
293
  return ('no browser session connected, but one WAS connected to this daemon earlier, so the wiring ' +
139
294
  'is correct. The tab was closed, navigated away, or hard-reloaded. Ask the human to reopen ' +
140
- `the app (or run \`reticle open\`), or reload the tab. ${SELF_SERVE} ${RETRY}`);
295
+ `the app (or run ${OPEN_CMD_BARE}), or reload the tab. ${SELF_SERVE} ${RETRY}`);
296
+ }
297
+ // Ranked ahead of every "we have never seen one" branch below, because it is the one fact that
298
+ // OUTRANKS them: those branches all reason from an absence, and this reasons from a recorded
299
+ // connection. It is also the branch that stops the daemon contradicting itself — a hint that the
300
+ // wiring is unproven, printed by a daemon holding proof that it is.
301
+ if (true === facts.previouslyConnected) {
302
+ const listeners = 0 === listening.length
303
+ ? `${unattributedListeners(listening)} If the dev server is not running, start it first ` +
304
+ '(the command is in `next_action`).'
305
+ : unattributedListeners(listening);
306
+ return `${RESTARTED_LEAD} ${OPEN_THE_APP} ${listeners} ${rankedCauses(facts)} ${SELF_SERVE} ${RETRY}`;
307
+ }
308
+ // A config found elsewhere outranks every "you may have no SDK" branch below: those reason from
309
+ // an absence in ONE directory, and this is a file we read in another. Ranked here so the reader is
310
+ // never sent through an install on a project that has demonstrably been installed.
311
+ if (!initialized && (facts.configsElsewhere ?? []).length > 0) {
312
+ return ('no browser session connected, and this daemon has never seen one. ' +
313
+ `${configsElsewhereClause(facts)} ${unattributedListeners(listening)} ${OPEN_THE_APP} ` +
314
+ `${rankedCauses(facts)} ${SELF_SERVE} ${RETRY}`);
141
315
  }
142
316
  if (0 === listening.length) {
143
317
  // Lead with the stronger EVIDENCE, and do not overstate what it is.
@@ -156,20 +330,25 @@ export function diagnoseNoSession(facts) {
156
330
  // is expected rather than diagnostic.
157
331
  if (!initialized) {
158
332
  return ('no browser session connected. Two things to weigh, and neither of them is proof. ' +
159
- `(1) Nothing is listening on the ports Reticle scans (${SCANNED_PORTS}), so the dev server ` +
160
- 'may not be running — ask the human to start it (`npm run dev`). That scan is narrow ' +
333
+ `(1) ${slowListenerClause(facts)}Nothing is listening on the ports Reticle scans (${SCANNED_PORTS}), so the dev server ` +
334
+ 'may not be running — START IT YOURSELF, in the background, using the command in ' +
335
+ "`next_action` (it is read from this project's own scripts; if there is none, that field " +
336
+ 'says so and you should ask rather than guess). Tell the human in one line that it is ' +
337
+ 'running. That scan is narrow ' +
161
338
  'though: a server on any other port is invisible to it, so if the app IS running, ask for ' +
162
- 'its URL rather than assuming it is down, and open it with `reticle open <url>`. ' +
339
+ `its URL rather than assuming it is down, and open it with ${OPEN_CMD}. ` +
163
340
  `(2) There is no \`.reticle.json\` in ${where}. That is the ` +
164
- "file `reticle init` writes, so the app may carry no Reticle SDK — but check the app's " +
341
+ `file ${INIT_CMD} writes, so the app may carry no Reticle SDK — but check the app's ` +
165
342
  'OWN directory before re-running `init`: in a monorepo the daemon often runs at the root ' +
166
343
  'while the app lives in a subdirectory, and an app wired by the Vite or Babel plugin ' +
167
- `carries the SDK without that file at all. ${RETRY}`);
344
+ `carries the SDK without that file at all.${searchedClause(facts)} ${URL_THEN_LEASE} ${RETRY}`);
168
345
  }
169
- return (`no browser session connected, and this daemon has never seen one. ${OPEN_THE_APP} ` +
346
+ return (`${stallClause(facts)}no browser session connected, and this daemon has never seen one. ${OPEN_THE_APP} ` +
170
347
  'Nothing is listening on the ports Reticle scans ' +
171
348
  `(${SCANNED_PORTS}) either, and the most common reason for that is a dev server that is not ` +
172
- 'running: ask the human to start it (`npm run dev`), then open the app in a browser. ' +
349
+ 'running: start it yourself in the background with the command in `next_action` — it is read ' +
350
+ "from this project's own scripts, and says so rather than guessing when there is none — tell " +
351
+ 'the human in one line that it is running, then open the app in a browser. ' +
173
352
  // The caveat is here rather than omitted because the scan is NARROW, and the old sentence
174
353
  // spent its confidence as though an empty result were proof of absence. Reported twice: a
175
354
  // scripted drive of 2.5.0 asserted the app was not running while it served 200 on :7699, and
@@ -180,7 +359,7 @@ export function diagnoseNoSession(facts) {
180
359
  // Deliberately NOT offering reticle_lease here, and a test pins that: a lease opens a URL, and
181
360
  // if nothing is listening there is nothing at any URL to open. Asking for the real one is the
182
361
  // only move that can recover the :7699 case.
183
- 'the app IS running, ask the human for its URL rather than assuming it is down. ' +
362
+ `the app IS running, ask the human for its URL rather than assuming it is down. ${URL_THEN_LEASE} ` +
184
363
  // Reachable only for a project that HAS been through `init` — the uninstrumented case is
185
364
  // answered above, leading with that certainty instead of behind this scan's guess.
186
365
  `${RETRY}`);
@@ -188,17 +367,15 @@ export function diagnoseNoSession(facts) {
188
367
  if (!initialized) {
189
368
  return ('no browser session connected, and this daemon has never seen one. ' +
190
369
  `What was actually checked: there is no \`.reticle.json\` in ${where}. That is the file ` +
191
- '`reticle init` writes, so the app may carry no Reticle SDK — but it is not proof, and the ' +
370
+ `${INIT_CMD} writes, so the app may carry no Reticle SDK — but it is not proof, and the ` +
192
371
  'same absence is expected in a monorepo whose daemon runs at the root while the app lives in ' +
193
372
  "a subdirectory, or in an app wired by the Vite or Babel plugin. Check the app's OWN " +
194
- 'directory: if it has no config, run `reticle init` there and restart the dev server; if it ' +
195
- 'has one, the app is wired and simply has no page open — `reticle open <url>`. ' +
196
- `${unattributedListeners(listening)} ${RETRY}`);
373
+ `directory: if it has no config, run ${INIT_CMD} there and restart the dev server; if it ` +
374
+ `has one, the app is wired and simply has no page open — ${OPEN_CMD}. ` +
375
+ `${unattributedListeners(listening)}${searchedClause(facts)} ${RETRY}`);
197
376
  }
198
- return ('no browser session connected, and this daemon has never seen one for this project, which is ' +
377
+ return (`${stallClause(facts)}no browser session connected, and this daemon has never seen one for this project, which is ` +
199
378
  `wired for Reticle. ${OPEN_THE_APP} ${unattributedListeners(listening)} ` +
200
- 'If the page IS open and still does not appear, the app is either serving a build made before ' +
201
- `the wiring landed or dialling a different daemon than this one (on ${String(port)}): restart ` +
202
- "the dev server, hard-reload the page, and check that the app's reticle port matches " +
203
- `${String(port)}.${portMismatchClause(facts)} ${NON_LOCALHOST_GATE} ${SELF_SERVE} ${RETRY}`);
379
+ 'If the page IS open and still does not appear, the app is wired and the SDK is not reaching ' +
380
+ `this daemon (on ${String(port)}). ${rankedCauses(facts)} ${SELF_SERVE} ${RETRY}`);
204
381
  }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * "No browser session connected", as something an agent can RUN.
3
+ *
4
+ * The prose diagnosis next door is good and it is prose: the agent has to read four hedged sentences
5
+ * and decide which of them is about its situation. This is the same decision, decided here, as one
6
+ * value plus one literal command — so the branch happens in the daemon, which has the evidence,
7
+ * rather than in the agent, which does not.
8
+ *
9
+ * The rule that outranks helpfulness: never invent a command. If the project has no dev script, this
10
+ * says there is none. A command that does not exist sends the agent chasing a phantom error in its
11
+ * own repo.
12
+ *
13
+ * Pure. The facts arrive from the watch; nothing here touches the disk or the clock.
14
+ */
15
+ import { NoSessionAction } from '@reticlehq/core';
16
+ import type { DevCommand } from './dev-command.js';
17
+ /** The executable half of the no-session payload. */
18
+ export interface NoSessionNextAction {
19
+ action: NoSessionAction;
20
+ /** The literal command to run. Absent whenever it could not be sourced without guessing. */
21
+ command?: string;
22
+ /** The port this action is about — where the app is, or where the dev script will put it. */
23
+ port?: number;
24
+ /** One sentence: why this is the action, and — when there is no command — why there is none. */
25
+ reason: string;
26
+ }
27
+ interface NextActionFacts {
28
+ everConnected: boolean;
29
+ initialized: boolean;
30
+ listening: readonly number[];
31
+ dev: DevCommand | undefined;
32
+ /**
33
+ * An app for this project has connected on this port before, from durable state.
34
+ *
35
+ * Outranks `initialized`, which is only ever "is there a `.reticle.json` in the ONE directory this
36
+ * daemon stands in". In a monorepo, or under an editor that starts the MCP server from the user's
37
+ * home, that answer is no for projects that are wired and have connected — and the action handed
38
+ * back was `reticle init`, which is the one action that cannot help and can overwrite the config
39
+ * that works.
40
+ */
41
+ previouslyConnected?: boolean;
42
+ }
43
+ export declare function nextActionFor(facts: NextActionFacts): NoSessionNextAction;
44
+ /** The same action as one sentence, for the prose message the human reads. */
45
+ export declare function renderNextAction(next: NoSessionNextAction): string;
46
+ export {};
@@ -0,0 +1,85 @@
1
+ /**
2
+ * "No browser session connected", as something an agent can RUN.
3
+ *
4
+ * The prose diagnosis next door is good and it is prose: the agent has to read four hedged sentences
5
+ * and decide which of them is about its situation. This is the same decision, decided here, as one
6
+ * value plus one literal command — so the branch happens in the daemon, which has the evidence,
7
+ * rather than in the agent, which does not.
8
+ *
9
+ * The rule that outranks helpfulness: never invent a command. If the project has no dev script, this
10
+ * says there is none. A command that does not exist sends the agent chasing a phantom error in its
11
+ * own repo.
12
+ *
13
+ * Pure. The facts arrive from the watch; nothing here touches the disk or the clock.
14
+ */
15
+ import { NoSessionAction } from '@reticlehq/core';
16
+ /** `reticle init`, the only command here that is Reticle's own and so cannot be wrong. */
17
+ const INIT_COMMAND = 'reticle init';
18
+ const OPEN_COMMAND = 'reticle open';
19
+ const LOCALHOST = 'http://localhost';
20
+ export function nextActionFor(facts) {
21
+ if (facts.everConnected) {
22
+ return {
23
+ action: NoSessionAction.REOPEN_APP,
24
+ reason: 'a session was connected to this daemon earlier, so the wiring is correct — the tab was ' +
25
+ 'closed, reloaded, or the lease aged out. Reopen the app, or take one you own with ' +
26
+ 'reticle_lease {action:"acquire", url}.',
27
+ };
28
+ }
29
+ if (0 === facts.listening.length) {
30
+ const dev = facts.dev;
31
+ if (dev === undefined) {
32
+ return {
33
+ action: NoSessionAction.START_DEV_SERVER,
34
+ reason: 'nothing is listening on the ports Reticle scans, so the app is probably not running — ' +
35
+ 'but this project declares no dev script (no `dev`, `develop` or `start` in its ' +
36
+ 'package.json), so there is no command to hand you. Ask the human how their app is ' +
37
+ 'started, and for the URL it serves on.',
38
+ };
39
+ }
40
+ return {
41
+ action: NoSessionAction.START_DEV_SERVER,
42
+ command: dev.command,
43
+ ...(dev.port === undefined ? {} : { port: dev.port }),
44
+ reason: 'nothing is listening on the ports Reticle scans, so the app is not running. This is the ' +
45
+ `project's own \`${dev.script}\` script — run it in the background, tell the human it is ` +
46
+ 'running, then call reticle_sessions again.',
47
+ };
48
+ }
49
+ // Something IS up. Whatever else is true, do NOT hand back a start command: a second dev server on
50
+ // a second port is the exact confusion the probe exists to prevent.
51
+ const ports = facts.listening.join(', ');
52
+ const only = 1 === facts.listening.length ? facts.listening[0] : undefined;
53
+ if (!facts.initialized && true !== facts.previouslyConnected) {
54
+ return {
55
+ action: NoSessionAction.RUN_INIT,
56
+ command: INIT_COMMAND,
57
+ ...(only === undefined ? {} : { port: only }),
58
+ reason: `something is listening (${ports}) but no \`.reticle.json\` was found, so the app may ` +
59
+ "carry no Reticle SDK. Run this in the APP's own directory — in a monorepo that is not " +
60
+ 'where this daemon stands — then restart the dev server.',
61
+ };
62
+ }
63
+ if (only === undefined) {
64
+ return {
65
+ action: NoSessionAction.OPEN_APP,
66
+ reason: `this project is wired and no page is open. Several ports are listening (${ports}) and ` +
67
+ 'none of them can be attributed to this project, so open the one the human names: ' +
68
+ `\`${OPEN_COMMAND} <url>\`.`,
69
+ };
70
+ }
71
+ return {
72
+ action: NoSessionAction.OPEN_APP,
73
+ command: `${OPEN_COMMAND} ${LOCALHOST}:${String(only)}`,
74
+ port: only,
75
+ reason: 'this project is wired and a dev server is listening — Reticle only ever sees a page a ' +
76
+ 'browser has LOADED, and nothing has loaded one.',
77
+ };
78
+ }
79
+ /** The same action as one sentence, for the prose message the human reads. */
80
+ export function renderNextAction(next) {
81
+ const command = next.command;
82
+ return command === undefined
83
+ ? `NEXT ACTION: ${next.reason}`
84
+ : `NEXT ACTION: run \`${command}\` — ${next.reason}`;
85
+ }
@@ -10,6 +10,42 @@
10
10
  * daemon that outlives the agent by hours has no business scanning ports it does not need.
11
11
  */
12
12
  import type { SessionManager } from './session-manager.js';
13
+ interface NoSessionWatchOptions {
14
+ sessions: SessionManager;
15
+ port: number;
16
+ /** Whether this project has been through `reticle init` (a projectId is stamped in .reticle.json). */
17
+ initialized: boolean;
18
+ /** Where that was decided — this daemon's working directory unless a caller says otherwise. */
19
+ directory?: string;
20
+ probe?: () => Promise<number[]>;
21
+ /**
22
+ * How many pooled leases have aged out, if a pool exists. Injected as a reader rather than the
23
+ * pool itself: the diagnosis needs one number, and taking the whole pool would tie the session
24
+ * layer to the browser layer for it.
25
+ */
26
+ reapedLeases?: () => number;
27
+ /**
28
+ * Opens a URL in a browser Reticle owns, and resolves once it is open.
29
+ *
30
+ * The daemon passes the POOL's acquire — the same path `reticle_lease` takes — rather than
31
+ * `reticle drive`. Deliberate: the pool runs inside this process and binds nothing, so it needs no
32
+ * port arbitration, while `reticle drive` starts a second daemon that fights this one for the port.
33
+ * Omitted on a daemon with no pool, and auto-attach is then simply off.
34
+ */
35
+ attach?: (url: string) => Promise<unknown>;
36
+ /**
37
+ * Where the durable "an app has connected here before" bit lives. The daemon's state home unless
38
+ * a test says otherwise.
39
+ */
40
+ stateDir?: string;
41
+ }
42
+ /**
43
+ * Start the watch. Returns a stop function; the timer is unref'd so it never holds the daemon up.
44
+ *
45
+ * Exported for the test that pins the after-boot config read below — the daemon itself starts this
46
+ * through `wireSessionScope`.
47
+ */
48
+ export declare function startNoSessionWatch(options: NoSessionWatchOptions): () => void;
13
49
  /**
14
50
  * The daemon's whole session-scoping decision in one call: scope auto-selection to the active
15
51
  * project, and keep the no-session diagnosis fresh. Both derive from the same one fact — whether
@@ -18,4 +54,7 @@ import type { SessionManager } from './session-manager.js';
18
54
  */
19
55
  export declare function wireSessionScope(sessions: SessionManager, activeProjectId: string | undefined, port: number,
20
56
  /** Reader for the pool's aged-out-lease count; omitted when this daemon runs no pool. */
21
- reapedLeases?: () => number): () => void;
57
+ reapedLeases?: () => number,
58
+ /** Opens a URL in a Reticle-owned browser (the pool). Omitted ⇒ auto-attach is off. */
59
+ attach?: (url: string) => Promise<unknown>): () => void;
60
+ export {};