@reticlehq/server 2.7.0 → 2.9.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 (357) hide show
  1. package/README.md +14 -5
  2. package/SKILL.md +130 -778
  3. package/dist/bridge/bridge.d.ts +23 -0
  4. package/dist/bridge/bridge.js +80 -2
  5. package/dist/bridge/bridge.test-harness.d.ts +5 -0
  6. package/dist/bridge/bridge.test-harness.js +6 -4
  7. package/dist/capsule/causal-summary.d.ts +36 -1
  8. package/dist/capsule/causal-summary.js +33 -7
  9. package/dist/capsule/divergence.js +7 -1
  10. package/dist/cli/chromium-hint.d.ts +33 -5
  11. package/dist/cli/chromium-hint.js +82 -12
  12. package/dist/cli/cli-doctor.js +65 -34
  13. package/dist/cli/cli-flow-commands.js +8 -8
  14. package/dist/cli/cli-kill.d.ts +58 -0
  15. package/dist/cli/cli-kill.js +162 -0
  16. package/dist/cli/cli-parse.d.ts +16 -1
  17. package/dist/cli/cli-parse.js +32 -9
  18. package/dist/cli/cli-port.d.ts +32 -0
  19. package/dist/cli/cli-port.js +82 -35
  20. package/dist/cli/cli-verify.d.ts +21 -0
  21. package/dist/cli/cli-verify.js +47 -3
  22. package/dist/cli/config-discovery.d.ts +35 -0
  23. package/dist/cli/config-discovery.js +163 -0
  24. package/dist/cli/doctor-daemon-line.js +4 -1
  25. package/dist/cli/doctor-project-line.d.ts +37 -0
  26. package/dist/cli/doctor-project-line.js +35 -0
  27. package/dist/cli/doctor-rows.d.ts +51 -0
  28. package/dist/cli/doctor-rows.js +54 -0
  29. package/dist/cli/doctor-sessions-line.d.ts +12 -0
  30. package/dist/cli/doctor-sessions-line.js +69 -0
  31. package/dist/cli/drive-attach.d.ts +99 -0
  32. package/dist/cli/drive-attach.js +203 -0
  33. package/dist/cli/drive-command.d.ts +13 -0
  34. package/dist/cli/drive-command.js +103 -0
  35. package/dist/cli/open-note.d.ts +20 -0
  36. package/dist/cli/open-note.js +37 -0
  37. package/dist/cli/port-holder.d.ts +10 -0
  38. package/dist/cli/port-holder.js +26 -2
  39. package/dist/cli/status-next-action.d.ts +37 -0
  40. package/dist/cli/status-next-action.js +61 -0
  41. package/dist/cli.js +119 -39
  42. package/dist/crawl/crawl-tools.js +12 -2
  43. package/dist/crawl/crawl.d.ts +13 -0
  44. package/dist/crawl/crawl.js +50 -2
  45. package/dist/events/contradictions.d.ts +46 -0
  46. package/dist/events/contradictions.js +35 -22
  47. package/dist/events/declared.d.ts +45 -0
  48. package/dist/events/declared.js +73 -0
  49. package/dist/events/echo-mismatch.d.ts +8 -2
  50. package/dist/events/echo-mismatch.js +33 -5
  51. package/dist/events/event-filters.d.ts +1 -1
  52. package/dist/events/event-filters.js +7 -4
  53. package/dist/events/predicate-eval.d.ts +17 -72
  54. package/dist/events/predicate-eval.js +117 -236
  55. package/dist/events/predicate-parse.js +31 -3
  56. package/dist/events/predicate-route.d.ts +32 -0
  57. package/dist/events/predicate-route.js +102 -0
  58. package/dist/events/predicate-schema.d.ts +99 -0
  59. package/dist/events/predicate-schema.js +309 -0
  60. package/dist/events/predicate.d.ts +8 -0
  61. package/dist/events/predicate.js +296 -22
  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-classify.js +1 -1
  69. package/dist/flows/flow-replay-run.js +8 -1
  70. package/dist/flows/flow-tools.js +21 -5
  71. package/dist/flows/heal-run.js +1 -1
  72. package/dist/flows/replay.js +3 -0
  73. package/dist/honesty/accepted-write.d.ts +3 -0
  74. package/dist/honesty/accepted-write.js +8 -3
  75. package/dist/honesty/already-true.js +15 -0
  76. package/dist/honesty/blind-spots.js +10 -2
  77. package/dist/honesty/honesty.d.ts +11 -0
  78. package/dist/honesty/honesty.js +1 -0
  79. package/dist/honesty/uncaptured-bodies.d.ts +4 -2
  80. package/dist/honesty/uncaptured-bodies.js +9 -2
  81. package/dist/honesty/unread-outcome.d.ts +8 -0
  82. package/dist/honesty/unread-outcome.js +36 -9
  83. package/dist/honesty/unsettled.d.ts +49 -0
  84. package/dist/honesty/unsettled.js +82 -0
  85. package/dist/honesty/verified.d.ts +36 -3
  86. package/dist/honesty/verified.js +54 -11
  87. package/dist/http-server.d.ts +19 -0
  88. package/dist/http-server.js +202 -4
  89. package/dist/impact/impact-recorder.d.ts +23 -0
  90. package/dist/impact/impact-recorder.js +76 -0
  91. package/dist/impact/impact-store.d.ts +47 -0
  92. package/dist/impact/impact-store.js +171 -0
  93. package/dist/index.d.ts +7 -0
  94. package/dist/index.js +47 -10
  95. package/dist/init/agent-rules.d.ts +36 -0
  96. package/dist/init/agent-rules.js +132 -22
  97. package/dist/init/astro-patch.d.ts +2 -1
  98. package/dist/init/astro-patch.js +38 -6
  99. package/dist/init/capabilities.d.ts +22 -0
  100. package/dist/init/capabilities.js +76 -0
  101. package/dist/init/confirm.d.ts +114 -0
  102. package/dist/init/confirm.js +165 -0
  103. package/dist/init/connect-steps.d.ts +2 -0
  104. package/dist/init/connect-steps.js +50 -0
  105. package/dist/init/cra.js +3 -2
  106. package/dist/init/csp-check.d.ts +38 -0
  107. package/dist/init/csp-check.js +92 -0
  108. package/dist/init/csp-doctor.d.ts +25 -0
  109. package/dist/init/csp-doctor.js +49 -0
  110. package/dist/init/cursor.js +6 -9
  111. package/dist/init/detect.d.ts +8 -0
  112. package/dist/init/detect.js +33 -3
  113. package/dist/init/mcp-clients.d.ts +1 -0
  114. package/dist/init/mcp-clients.js +51 -7
  115. package/dist/init/mcp.d.ts +15 -0
  116. package/dist/init/mcp.js +19 -0
  117. package/dist/init/non-js-project.js +29 -5
  118. package/dist/init/plan-framework.d.ts +10 -0
  119. package/dist/init/plan-framework.js +78 -16
  120. package/dist/init/plan.d.ts +47 -6
  121. package/dist/init/plan.js +228 -69
  122. package/dist/init/run.d.ts +24 -2
  123. package/dist/init/run.js +157 -45
  124. package/dist/init/slash-command.d.ts +1 -1
  125. package/dist/init/slash-command.js +54 -6
  126. package/dist/init/snippets.d.ts +45 -5
  127. package/dist/init/snippets.js +194 -42
  128. package/dist/init/vite-config.js +53 -5
  129. package/dist/input/network-mock-tools.d.ts +19 -0
  130. package/dist/input/network-mock-tools.js +37 -5
  131. package/dist/input/scroll-find.d.ts +6 -0
  132. package/dist/input/scroll-find.js +17 -2
  133. package/dist/input/scroll-tools.js +4 -0
  134. package/dist/journal/attach-journal.d.ts +9 -1
  135. package/dist/journal/attach-journal.js +13 -2
  136. package/dist/journal/journal-query.d.ts +13 -0
  137. package/dist/journal/journal-query.js +28 -3
  138. package/dist/journal/workspace-gitignore.d.ts +10 -0
  139. package/dist/journal/workspace-gitignore.js +59 -0
  140. package/dist/license/license.js +1 -1
  141. package/dist/mcp/mcp-proxy.d.ts +35 -30
  142. package/dist/mcp/mcp-proxy.js +306 -104
  143. package/dist/mcp/mcp.d.ts +10 -1
  144. package/dist/mcp/mcp.js +70 -34
  145. package/dist/mcp/proxy-handshake.d.ts +82 -1
  146. package/dist/mcp/proxy-handshake.js +109 -2
  147. package/dist/mcp/proxy-lifecycle.d.ts +11 -2
  148. package/dist/mcp/proxy-lifecycle.js +12 -3
  149. package/dist/mcp/proxy-log.d.ts +63 -0
  150. package/dist/mcp/proxy-log.js +132 -0
  151. package/dist/mcp/server-instructions.d.ts +17 -0
  152. package/dist/mcp/server-instructions.js +56 -0
  153. package/dist/mcp/shared-params.d.ts +27 -0
  154. package/dist/mcp/shared-params.js +36 -0
  155. package/dist/pool/browser-pool.d.ts +22 -2
  156. package/dist/pool/browser-pool.js +44 -4
  157. package/dist/project/learned-routes.d.ts +24 -0
  158. package/dist/project/learned-routes.js +63 -0
  159. package/dist/project/project-store.d.ts +6 -0
  160. package/dist/project/project-store.js +25 -1
  161. package/dist/project/reticle-dir.d.ts +2 -0
  162. package/dist/project/reticle-dir.js +1 -0
  163. package/dist/session/connection-memory.d.ts +59 -0
  164. package/dist/session/connection-memory.js +108 -0
  165. package/dist/session/dev-command.d.ts +26 -0
  166. package/dist/session/dev-command.js +89 -0
  167. package/dist/session/dev-server-probe.d.ts +68 -0
  168. package/dist/session/dev-server-probe.js +102 -7
  169. package/dist/session/last-act.d.ts +26 -0
  170. package/dist/session/last-act.js +35 -1
  171. package/dist/session/live-control-tools.js +42 -5
  172. package/dist/session/no-session-diagnosis.d.ts +67 -2
  173. package/dist/session/no-session-diagnosis.js +236 -27
  174. package/dist/session/no-session-next-action.d.ts +46 -0
  175. package/dist/session/no-session-next-action.js +85 -0
  176. package/dist/session/no-session-watch.d.ts +40 -1
  177. package/dist/session/no-session-watch.js +174 -8
  178. package/dist/session/pending-commands.d.ts +12 -0
  179. package/dist/session/pending-commands.js +13 -1
  180. package/dist/session/ref-provenance.d.ts +54 -0
  181. package/dist/session/ref-provenance.js +61 -0
  182. package/dist/session/session-lease.js +1 -1
  183. package/dist/session/session-manager.d.ts +4 -0
  184. package/dist/session/session-manager.js +35 -5
  185. package/dist/session/session.d.ts +10 -1
  186. package/dist/session/session.js +146 -5
  187. package/dist/telemetry/app-instrumented.d.ts +8 -0
  188. package/dist/telemetry/app-instrumented.js +10 -0
  189. package/dist/telemetry/automation-hint.d.ts +17 -0
  190. package/dist/telemetry/automation-hint.js +71 -0
  191. package/dist/telemetry/bug-found.d.ts +15 -17
  192. package/dist/telemetry/bug-found.js +52 -21
  193. package/dist/telemetry/cli-telemetry.js +7 -1
  194. package/dist/telemetry/daemon-telemetry.js +24 -0
  195. package/dist/telemetry/feedback.js +1 -1
  196. package/dist/telemetry/init-telemetry.js +9 -1
  197. package/dist/telemetry/install-source.d.ts +56 -0
  198. package/dist/telemetry/install-source.js +74 -0
  199. package/dist/telemetry/instrumentation-stall.d.ts +50 -0
  200. package/dist/telemetry/instrumentation-stall.js +90 -0
  201. package/dist/telemetry/run-telemetry.js +4 -1
  202. package/dist/telemetry/session-metrics.d.ts +8 -0
  203. package/dist/telemetry/session-metrics.js +15 -0
  204. package/dist/telemetry/telemetry.d.ts +7 -1
  205. package/dist/telemetry/telemetry.js +26 -2
  206. package/dist/telemetry/tool-refused.d.ts +19 -0
  207. package/dist/telemetry/tool-refused.js +55 -0
  208. package/dist/telemetry/verification-of.js +46 -7
  209. package/dist/temp-dir.d.ts +27 -0
  210. package/dist/temp-dir.js +46 -0
  211. package/dist/tools/act-tools.js +209 -28
  212. package/dist/tools/assert-grade.d.ts +1 -1
  213. package/dist/tools/assert-grade.js +27 -5
  214. package/dist/tools/assert-verdict.js +49 -7
  215. package/dist/tools/browser-tools.js +10 -0
  216. package/dist/tools/contract-tools.js +4 -0
  217. package/dist/tools/dynamic-tools.js +16 -7
  218. package/dist/tools/error-recovery.d.ts +10 -0
  219. package/dist/tools/error-recovery.js +73 -9
  220. package/dist/tools/feedback-invite.js +1 -1
  221. package/dist/tools/invoke-tool.js +159 -26
  222. package/dist/tools/lease-hint.d.ts +30 -8
  223. package/dist/tools/lease-hint.js +65 -14
  224. package/dist/tools/lease-tools.d.ts +40 -1
  225. package/dist/tools/lease-tools.js +181 -83
  226. package/dist/tools/navigate-result.js +1 -1
  227. package/dist/tools/observe-tools.js +10 -4
  228. package/dist/tools/observed-nothing.js +1 -1
  229. package/dist/tools/react-grace.d.ts +52 -0
  230. package/dist/tools/react-grace.js +90 -0
  231. package/dist/tools/read-tools.js +67 -5
  232. package/dist/tools/resolve-target.d.ts +33 -0
  233. package/dist/tools/resolve-target.js +53 -0
  234. package/dist/tools/settle-in-flight.d.ts +94 -0
  235. package/dist/tools/settle-in-flight.js +146 -0
  236. package/dist/tools/snapshot-delta.d.ts +10 -0
  237. package/dist/tools/snapshot-delta.js +194 -9
  238. package/dist/tools/tool-kit.d.ts +8 -0
  239. package/dist/tools/tool-surface.d.ts +60 -0
  240. package/dist/tools/tool-surface.js +86 -1
  241. package/dist/tools/tools.js +61 -8
  242. package/dist/tools/unadvertised-help.js +9 -6
  243. package/docs/README.md +55 -11
  244. package/docs/actions.mdx +120 -0
  245. package/docs/agent-cheatsheet.md +95 -29
  246. package/docs/architecture.md +52 -35
  247. package/docs/benchmarks.md +127 -75
  248. package/docs/best-practices.mdx +216 -0
  249. package/docs/capabilities.mdx +534 -0
  250. package/docs/cli/affected.mdx +53 -0
  251. package/docs/cli/capsules.mdx +70 -0
  252. package/docs/cli/cloud.mdx +153 -0
  253. package/docs/cli/doctor.mdx +109 -0
  254. package/docs/cli/drive.mdx +74 -0
  255. package/docs/cli/feedback.mdx +67 -0
  256. package/docs/cli/gate.mdx +64 -0
  257. package/docs/cli/help.mdx +94 -0
  258. package/docs/cli/hunt.mdx +73 -0
  259. package/docs/cli/identify.mdx +76 -0
  260. package/docs/cli/init.mdx +121 -0
  261. package/docs/cli/kill.mdx +91 -0
  262. package/docs/cli/license.mdx +48 -0
  263. package/docs/cli/mcp.mdx +61 -0
  264. package/docs/cli/open.mdx +74 -0
  265. package/docs/cli/restart.mdx +60 -0
  266. package/docs/cli/rollback.mdx +35 -0
  267. package/docs/cli/serve.mdx +69 -0
  268. package/docs/cli/status.mdx +88 -0
  269. package/docs/cli/stop.mdx +64 -0
  270. package/docs/cli/telemetry.mdx +57 -0
  271. package/docs/cli/update.mdx +52 -0
  272. package/docs/cli/verify.mdx +76 -0
  273. package/docs/cli/version.mdx +39 -0
  274. package/docs/cli/watch.mdx +62 -0
  275. package/docs/cli.mdx +241 -0
  276. package/docs/debugging.md +16 -10
  277. package/docs/deploy-checks.md +45 -12
  278. package/docs/desktop.mdx +384 -0
  279. package/docs/docs.json +364 -0
  280. package/docs/enterprise.md +22 -16
  281. package/docs/faq.mdx +102 -0
  282. package/docs/fixtures.md +13 -7
  283. package/docs/flows.md +29 -23
  284. package/docs/for-agents.md +71 -0
  285. package/docs/frameworks.mdx +238 -0
  286. package/docs/gate-plan.md +48 -42
  287. package/docs/gates.md +28 -22
  288. package/docs/getting-started.md +129 -77
  289. package/docs/human-control.md +24 -18
  290. package/docs/index.mdx +143 -0
  291. package/docs/install-agentic.mdx +224 -0
  292. package/docs/install-manual.mdx +196 -0
  293. package/docs/instrumentation.mdx +238 -0
  294. package/docs/integration-patterns.md +41 -35
  295. package/docs/local-registry.md +19 -13
  296. package/docs/multi-agent-testing.md +19 -13
  297. package/docs/packages/babel-plugin.mdx +79 -0
  298. package/docs/packages/browser.mdx +111 -0
  299. package/docs/packages/core.mdx +116 -0
  300. package/docs/packages/electron.mdx +85 -0
  301. package/docs/packages/eslint-plugin.mdx +86 -0
  302. package/docs/packages/next.mdx +89 -0
  303. package/docs/packages/react.mdx +91 -0
  304. package/docs/packages/server.mdx +98 -0
  305. package/docs/packages/tauri.mdx +63 -0
  306. package/docs/packages/test.mdx +119 -0
  307. package/docs/packages/vite-plugin.mdx +94 -0
  308. package/docs/packages.mdx +218 -0
  309. package/docs/platform-integration.md +35 -26
  310. package/docs/predicates.mdx +248 -0
  311. package/docs/quickstart.mdx +257 -0
  312. package/docs/recipes.mdx +282 -0
  313. package/docs/skill-file.mdx +107 -0
  314. package/docs/state-management.mdx +199 -0
  315. package/docs/style.css +14 -0
  316. package/docs/system-map.md +50 -33
  317. package/docs/telemetry-contract.md +106 -52
  318. package/docs/telemetry-events.mdx +475 -0
  319. package/docs/telemetry.md +63 -55
  320. package/docs/testing.md +44 -6
  321. package/docs/token-efficiency.md +37 -27
  322. package/docs/tools-act-and-wait.mdx +173 -0
  323. package/docs/tools-act-sequence.mdx +137 -0
  324. package/docs/tools-act.mdx +95 -0
  325. package/docs/tools-assert.mdx +129 -0
  326. package/docs/tools-console.mdx +63 -0
  327. package/docs/tools-inspect.mdx +88 -0
  328. package/docs/tools-navigate.mdx +99 -0
  329. package/docs/tools-network.mdx +119 -0
  330. package/docs/tools-observe.mdx +145 -0
  331. package/docs/tools-overview.mdx +160 -0
  332. package/docs/tools-query.mdx +126 -0
  333. package/docs/tools-session-and-feedback.mdx +112 -0
  334. package/docs/tools-sessions.mdx +94 -0
  335. package/docs/tools-snapshot.mdx +129 -0
  336. package/docs/tools-state.mdx +104 -0
  337. package/docs/tools-tools-and-run.mdx +156 -0
  338. package/docs/tools-wait-for.mdx +85 -0
  339. package/docs/troubleshooting.mdx +263 -0
  340. package/docs/usage.md +209 -165
  341. package/docs/vs-chrome-devtools-mcp.mdx +123 -0
  342. package/docs/vs-playwright-mcp.mdx +125 -0
  343. package/docs/vs-screenshots.mdx +137 -0
  344. package/docs/what-is-reticle.mdx +63 -0
  345. package/docs/why-reticle.mdx +109 -0
  346. package/package.json +5 -5
  347. package/docs/desktop-apps.md +0 -256
  348. package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
  349. package/docs/matrix/2.5.0/codex-machine.json +0 -20
  350. package/docs/matrix/2.5.0/cursor-machine.json +0 -22
  351. package/docs/matrix/2.5.0/gemini-machine.json +0 -22
  352. package/docs/matrix/2.5.0/opencode-machine.json +0 -22
  353. package/docs/matrix/2.5.0/vscode-machine.json +0 -22
  354. package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
  355. package/docs/matrix/MATRIX.md +0 -26
  356. package/docs/matrix/README.md +0 -59
  357. package/docs/mint.json +0 -61
@@ -9,23 +9,126 @@
9
9
  * It only probes while NOTHING is connected. Once a session is live the question is moot, and a
10
10
  * daemon that outlives the agent by hours has no business scanning ports it does not need.
11
11
  */
12
- import { probeDevServers } from './dev-server-probe.js';
12
+ import { probeDevServers, probeDevServerStates } from './dev-server-probe.js';
13
13
  import { diagnoseNoSession } from './no-session-diagnosis.js';
14
+ import { detectDevCommand } from './dev-command.js';
15
+ import { nextActionFor, renderNextAction } from './no-session-next-action.js';
16
+ import { readProjectFramework, readProjectId, readProjectPort } from '../cli/cli-port.js';
17
+ import { discoverProjectConfigs } from '../cli/config-discovery.js';
18
+ import { hasProjectConnectedBefore, rememberConnected } from './connection-memory.js';
19
+ import { reticleStateHome } from '../daemon/daemon.js';
14
20
  /** Slow enough to be free, fast enough that a dev server started 15s ago is already reflected. */
15
21
  const REFRESH_MS = 15_000;
16
- /** Start the watch. Returns a stop function; the timer is unref'd so it never holds the daemon up. */
17
- function startNoSessionWatch(options) {
22
+ /** The URL an auto-attach opens. `localhost` for the same reason the probe uses it: both stacks. */
23
+ const LOCALHOST = 'http://localhost';
24
+ /**
25
+ * What the failure path says, always naming the reason rather than swallowing it.
26
+ *
27
+ * An auto-attach that fails silently is the same class of defect as the message it replaces: the
28
+ * agent is told absence and never learns that Reticle tried and could not.
29
+ */
30
+ function attachFailureClause(port, reason) {
31
+ const collision = /EADDRINUSE|address already in use/i.test(reason);
32
+ return (` Reticle also tried to open ${LOCALHOST}:${String(port)} in a browser it owns and could not: ` +
33
+ `${reason}.` +
34
+ (collision
35
+ ? ' That is a port collision — something already holds the port this daemon needs, so the ' +
36
+ 'browser Reticle drives could not be started. Stop the other Reticle process and retry, ' +
37
+ 'rather than treating this as a problem with the app.'
38
+ : ''));
39
+ }
40
+ /**
41
+ * Start the watch. Returns a stop function; the timer is unref'd so it never holds the daemon up.
42
+ *
43
+ * Exported for the test that pins the after-boot config read below — the daemon itself starts this
44
+ * through `wireSessionScope`.
45
+ */
46
+ export function startNoSessionWatch(options) {
18
47
  const probe = options.probe ?? (() => probeDevServers());
19
48
  let listening = [];
49
+ /**
50
+ * Ports that accepted a connection and then answered nothing in time.
51
+ *
52
+ * Tracked beside `listening` rather than folded into it: they are not dev servers as far as the
53
+ * probe knows, so they must not be spent as proof the app is up, and they are not absent either,
54
+ * so the diagnosis must stop telling people to start a server that is already running. A custom
55
+ * `options.probe` returns only the serving set, so this stays empty for injected probes.
56
+ */
57
+ let slowListeners = [];
20
58
  let running = false;
59
+ /** Ports auto-attach has already spent its one attempt on. Bounded: never a loop, never a retry. */
60
+ const attempted = new Set();
61
+ /** Set only when an attempt actually failed, so the diagnosis can say so instead of hiding it. */
62
+ let attachFailure;
63
+ const directory = options.directory ?? process.cwd();
64
+ // The boot answer still counts (it is what the daemon scoped its sessions with), but `.reticle.json`
65
+ // is routinely written by `init` AFTER this daemon started, so re-read rather than cache. See the
66
+ // `initialized` comment below, which this shares.
67
+ const isWired = () => options.initialized || readProjectId(directory) !== undefined;
68
+ // Read at boot: the daemon's own identity does not change under it, and this is the key the
69
+ // durable bit is stored under.
70
+ const stateDir = options.stateDir ?? reticleStateHome();
71
+ /**
72
+ * Only ever asked ABOUT A NAMED PROJECT.
73
+ *
74
+ * With no projectId the memory can still answer the weaker "has anything connected on this port",
75
+ * and that answer must not be spent here: a shared 4400 on a machine with several repos would
76
+ * then soften a genuinely-unwired directory's diagnosis on the strength of an unrelated app. That
77
+ * is the same over-confident claim this whole file exists to remove, pointing the other way.
78
+ */
79
+ const connectedBefore = () => hasProjectConnectedBefore(stateDir, options.port, readProjectId(directory));
80
+ // Every path that registers a session goes through SessionManager.add, so this is the one hook
81
+ // that makes the bit durable. Recorded per port + projectId so a shared 4400 cannot make one
82
+ // project's success into evidence about another's.
83
+ options.sessions.setConnectionRecorder((projectId) => {
84
+ rememberConnected(stateDir, options.port, projectId);
85
+ });
86
+ /**
87
+ * Open the app ourselves when there is exactly one unambiguous candidate.
88
+ *
89
+ * AUTOMATIC, not offered — the deliberate call. Offering means a round trip and a decision by an
90
+ * agent that has strictly less evidence than this daemon does, on the one case where there is
91
+ * nothing left to decide: the project is wired, so a page WILL connect, and exactly one dev server
92
+ * is listening, so there is no ambiguity about which. That case is the bulk of the loss. Every
93
+ * other shape (several listeners, an unwired project, no listener) still returns prose, because
94
+ * there the daemon would be guessing and a guess that opens a browser is worse than a sentence.
95
+ *
96
+ * Bounded by construction: one attempt per port per daemon. A failure is recorded, never retried —
97
+ * a retry loop against a broken Chromium install would spin for the daemon's whole life.
98
+ */
99
+ const autoAttach = async (ports) => {
100
+ const attach = options.attach;
101
+ if (attach === undefined)
102
+ return;
103
+ if (1 !== ports.length)
104
+ return;
105
+ const [only] = ports;
106
+ if (only === undefined || attempted.has(only))
107
+ return;
108
+ if (!isWired())
109
+ return;
110
+ attempted.add(only);
111
+ try {
112
+ await attach(`${LOCALHOST}:${String(only)}`);
113
+ }
114
+ catch (error) {
115
+ attachFailure = attachFailureClause(only, error instanceof Error ? error.message : String(error));
116
+ }
117
+ };
21
118
  const refresh = () => {
22
119
  // Nothing to diagnose while a session is live, and no reason to scan.
23
120
  if (running || options.sessions.count() > 0)
24
121
  return;
25
122
  running = true;
26
- void probe()
27
- .then((ports) => {
123
+ void (options.probe === undefined
124
+ ? probeDevServerStates().then((states) => {
125
+ slowListeners = states.slow;
126
+ return states.serving;
127
+ })
128
+ : probe())
129
+ .then(async (ports) => {
28
130
  listening = ports;
131
+ await autoAttach(ports);
29
132
  })
30
133
  .catch(() => {
31
134
  /* a diagnostic hint must never take the daemon down */
@@ -37,16 +140,76 @@ function startNoSessionWatch(options) {
37
140
  refresh();
38
141
  const timer = setInterval(refresh, REFRESH_MS);
39
142
  timer.unref();
143
+ const nextAction = () => nextActionFor({
144
+ everConnected: options.sessions.everConnected(),
145
+ initialized: isWired(),
146
+ previouslyConnected: connectedBefore(),
147
+ listening,
148
+ // Read when asked, like everything else here: a `package.json` can gain a dev script, and a
149
+ // daemon that cached "there is none" at boot would keep saying so for the rest of the day.
150
+ dev: detectDevCommand(directory),
151
+ });
152
+ options.sessions.setNoSessionNextAction(nextAction);
40
153
  options.sessions.setNoSessionHint(() => diagnoseNoSession({
41
154
  everConnected: options.sessions.everConnected(),
42
- initialized: options.initialized,
155
+ // Read WHEN ASKED, for the same reason `projectPort` below is: `.reticle.json` is routinely
156
+ // written by `init` after this daemon started — that is the ordinary first-install order — and
157
+ // the boot-time answer is then permanently stale. Reported from the field as `reticle status`
158
+ // saying the project had never been through `init` about a project whose config named its
159
+ // framework and its projectId, and whose real problem was a dev server older than the plugin.
160
+ // The boot value still counts: it is the one the daemon scoped its sessions with.
161
+ initialized: isWired(),
43
162
  listening,
163
+ slowListeners,
44
164
  port: options.port,
165
+ // The directory `initialized` was decided in. Named in the message because "there is no
166
+ // `.reticle.json`" is a claim about ONE directory, and a reader standing somewhere else
167
+ // cannot tell whether it is a claim about their app at all.
168
+ directory,
169
+ // Where the config actually is, when it is not here — and where we looked, when it is
170
+ // nowhere. Computed only on the branch that needs it: a wired daemon has its answer already,
171
+ // and a workspace scan on every hint would be a directory walk per tool refusal.
172
+ ...(isWired()
173
+ ? {}
174
+ : (() => {
175
+ const discovery = discoverProjectConfigs(directory);
176
+ const elsewhere = discovery.found.filter((c) => c.directory !== directory);
177
+ return 0 === elsewhere.length
178
+ ? { searchedDirectories: discovery.searched }
179
+ : {
180
+ configsElsewhere: elsewhere.map((c) => ({
181
+ directory: c.directory,
182
+ ...(c.projectId === undefined ? {} : { projectId: c.projectId }),
183
+ })),
184
+ };
185
+ })()),
186
+ // The one fact that outranks every absence below it, and the reason a fresh daemon stopped
187
+ // claiming that an install which has demonstrably worked has never worked.
188
+ previouslyConnected: connectedBefore(),
189
+ // Ranks the causes. Read when asked, like the rest: `init` writes this file after the daemon
190
+ // starts on an ordinary first install.
191
+ ...(() => {
192
+ const framework = readProjectFramework(directory);
193
+ return framework === undefined ? {} : { framework };
194
+ })(),
45
195
  leaseExpired: (options.reapedLeases?.() ?? 0) > 0,
46
- }));
196
+ // Read here rather than at boot: `.reticle.json` can be written by `init` after this daemon
197
+ // started, which is the ordinary first-install order, and a port cached from before it existed
198
+ // would make the daemon confidently report no mismatch on the one run where there is one.
199
+ ...(() => {
200
+ const configured = readProjectPort(directory);
201
+ return configured === undefined ? {} : { projectPort: configured };
202
+ })(),
203
+ }) +
204
+ // Prose for the human, then the literal command for the agent. Appended rather than folded
205
+ // into the diagnosis so that file stays pure and its cases stay independently pinned.
206
+ ` ${renderNextAction(nextAction())}` +
207
+ (attachFailure ?? ''));
47
208
  return () => {
48
209
  clearInterval(timer);
210
+ options.sessions.setConnectionRecorder(undefined);
49
211
  options.sessions.setNoSessionHint(undefined);
212
+ options.sessions.setNoSessionNextAction(undefined);
50
213
  };
51
214
  }
52
215
  /**
@@ -57,7 +220,9 @@ function startNoSessionWatch(options) {
57
220
  */
58
221
  export function wireSessionScope(sessions, activeProjectId, port,
59
222
  /** Reader for the pool's aged-out-lease count; omitted when this daemon runs no pool. */
60
- reapedLeases) {
223
+ reapedLeases,
224
+ /** Opens a URL in a Reticle-owned browser (the pool). Omitted ⇒ auto-attach is off. */
225
+ attach) {
61
226
  if (activeProjectId !== undefined)
62
227
  sessions.setDefaultScope({ projectId: activeProjectId });
63
228
  return startNoSessionWatch({
@@ -65,5 +230,6 @@ reapedLeases) {
65
230
  port,
66
231
  initialized: activeProjectId !== undefined,
67
232
  ...(reapedLeases === undefined ? {} : { reapedLeases }),
233
+ ...(attach === undefined ? {} : { attach }),
68
234
  });
69
235
  }
@@ -1,4 +1,16 @@
1
1
  import type { CommandResult } from '@reticlehq/core';
2
+ /**
3
+ * The page was asked and did not answer inside the caller's budget.
4
+ *
5
+ * A distinct type because the two ways a command can fail mean opposite things, and a caller that
6
+ * cannot tell them apart will do the wrong thing with one of them. A rejection from a replaced
7
+ * transport says the question never reached the page and is worth asking again; a timeout says it
8
+ * did reach the page, which did not answer in the time granted — and asking again spends a budget
9
+ * that is already gone. Retrying a timeout as though it were a disconnect turned one read into
10
+ * hundreds of commands on the wire against a page that was reconnecting in a loop.
11
+ */
12
+ export declare class CommandTimeoutError extends Error {
13
+ }
2
14
  export declare class PendingCommands {
3
15
  #private;
4
16
  /** Next correlation id. Monotonic per session, so a reply can never match an earlier command. */
@@ -1,3 +1,15 @@
1
+ /**
2
+ * The page was asked and did not answer inside the caller's budget.
3
+ *
4
+ * A distinct type because the two ways a command can fail mean opposite things, and a caller that
5
+ * cannot tell them apart will do the wrong thing with one of them. A rejection from a replaced
6
+ * transport says the question never reached the page and is worth asking again; a timeout says it
7
+ * did reach the page, which did not answer in the time granted — and asking again spends a budget
8
+ * that is already gone. Retrying a timeout as though it were a disconnect turned one read into
9
+ * hundreds of commands on the wire against a page that was reconnecting in a loop.
10
+ */
11
+ export class CommandTimeoutError extends Error {
12
+ }
1
13
  export class PendingCommands {
2
14
  #pending = new Map();
3
15
  #seq = 0;
@@ -11,7 +23,7 @@ export class PendingCommands {
11
23
  return new Promise((resolve, reject) => {
12
24
  const timer = setTimeout(() => {
13
25
  this.#pending.delete(id);
14
- reject(new Error(describeTimeout()));
26
+ reject(new CommandTimeoutError(describeTimeout()));
15
27
  }, timeoutMs);
16
28
  timer.unref();
17
29
  this.#pending.set(id, { resolve, reject, timer });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Which tab a ref came out of, so the call that spends it cannot be routed to a different one.
3
+ *
4
+ * Reported from the field: three tabs of one project were connected, a snapshot was taken against a
5
+ * named session, and the act that followed omitted `sessionId`. Auto-selection picked a different
6
+ * tab, the ref legitimately did not exist there, and the refusal read "that ref is stale: refs are
7
+ * invalidated whenever the DOM re-renders" — a confident diagnosis of the wrong thing, and one that
8
+ * accuses the app under test. The recovery text then sends the agent to re-snapshot, which mints a
9
+ * fresh ref in whichever tab is chosen next, so the lesson it teaches is wrong either way.
10
+ *
11
+ * The quiet outcome is the worse one. Refs are minted from a per-document block, so the same number
12
+ * is a real, live, DIFFERENT element in the tab that was guessed: the identical call on a luckier day
13
+ * drives the wrong tab and returns a green about a page nobody asked about. That is a false green
14
+ * with extra steps, and it is the failure class this product exists to remove.
15
+ *
16
+ * Every `sessionId` description on the surface already promises that Reticle "refuses rather than
17
+ * guesses when ambiguous". This is the fact that makes the ambiguity visible: the tab a ref was
18
+ * handed out of is known at the moment it is handed out. Resolution is otherwise untouched — a call
19
+ * that names a session, carries no ref, or lands on the tab it looked at resolves silently, which is
20
+ * the cost the surface wording was rewritten to avoid.
21
+ *
22
+ * ponytail: one slot, not a per-ref map. It answers the reported shape — look at one tab, act on
23
+ * what you saw — for the cost of an assignment. A drive that interleaves refs from several tabs
24
+ * inside one turn needs per-ref provenance, and that is the upgrade if it ever shows up.
25
+ */
26
+ /** Record that a tool handed out refs from this session. */
27
+ export declare function noteRefsMinted(sessionId: string): void;
28
+ export declare function refsLastMintedIn(): string | undefined;
29
+ /** Test seam: the module holds one slot for the process, so a test has to be able to clear it. */
30
+ export declare function forgetRefProvenance(): void;
31
+ interface RefRouting {
32
+ /** The ref this call is about to spend, if it carries one. */
33
+ ref: string | undefined;
34
+ /** The `sessionId` the CALLER passed — when present, nothing was guessed. */
35
+ explicitSessionId: string | undefined;
36
+ /** The session resolution actually picked. */
37
+ chosenSessionId: string;
38
+ /**
39
+ * The connected sessions, read ONLY once a refusal is certain. A thunk because listing them walks
40
+ * every session and builds a health block each time, and the answer is wanted on the rare path.
41
+ */
42
+ connected: () => readonly {
43
+ sessionId: string;
44
+ url: string;
45
+ }[];
46
+ }
47
+ /**
48
+ * The refusal for a ref that belongs to another tab, or undefined when there is no ambiguity.
49
+ *
50
+ * Silent when the minting tab is no longer connected: that ref really is dead, and the stale-ref
51
+ * refusal that already exists is the honest answer for it.
52
+ */
53
+ export declare function wrongTabRefusal(routing: RefRouting): string | undefined;
54
+ export {};
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Which tab a ref came out of, so the call that spends it cannot be routed to a different one.
3
+ *
4
+ * Reported from the field: three tabs of one project were connected, a snapshot was taken against a
5
+ * named session, and the act that followed omitted `sessionId`. Auto-selection picked a different
6
+ * tab, the ref legitimately did not exist there, and the refusal read "that ref is stale: refs are
7
+ * invalidated whenever the DOM re-renders" — a confident diagnosis of the wrong thing, and one that
8
+ * accuses the app under test. The recovery text then sends the agent to re-snapshot, which mints a
9
+ * fresh ref in whichever tab is chosen next, so the lesson it teaches is wrong either way.
10
+ *
11
+ * The quiet outcome is the worse one. Refs are minted from a per-document block, so the same number
12
+ * is a real, live, DIFFERENT element in the tab that was guessed: the identical call on a luckier day
13
+ * drives the wrong tab and returns a green about a page nobody asked about. That is a false green
14
+ * with extra steps, and it is the failure class this product exists to remove.
15
+ *
16
+ * Every `sessionId` description on the surface already promises that Reticle "refuses rather than
17
+ * guesses when ambiguous". This is the fact that makes the ambiguity visible: the tab a ref was
18
+ * handed out of is known at the moment it is handed out. Resolution is otherwise untouched — a call
19
+ * that names a session, carries no ref, or lands on the tab it looked at resolves silently, which is
20
+ * the cost the surface wording was rewritten to avoid.
21
+ *
22
+ * ponytail: one slot, not a per-ref map. It answers the reported shape — look at one tab, act on
23
+ * what you saw — for the cost of an assignment. A drive that interleaves refs from several tabs
24
+ * inside one turn needs per-ref provenance, and that is the upgrade if it ever shows up.
25
+ */
26
+ /** The session the last ref-bearing result was produced from. Undefined until something mints one. */
27
+ let mintedIn;
28
+ /** Record that a tool handed out refs from this session. */
29
+ export function noteRefsMinted(sessionId) {
30
+ mintedIn = sessionId;
31
+ }
32
+ export function refsLastMintedIn() {
33
+ return mintedIn;
34
+ }
35
+ /** Test seam: the module holds one slot for the process, so a test has to be able to clear it. */
36
+ export function forgetRefProvenance() {
37
+ mintedIn = undefined;
38
+ }
39
+ /**
40
+ * The refusal for a ref that belongs to another tab, or undefined when there is no ambiguity.
41
+ *
42
+ * Silent when the minting tab is no longer connected: that ref really is dead, and the stale-ref
43
+ * refusal that already exists is the honest answer for it.
44
+ */
45
+ export function wrongTabRefusal(routing) {
46
+ if (routing.ref === undefined || routing.explicitSessionId !== undefined)
47
+ return undefined;
48
+ const source = mintedIn;
49
+ if (source === undefined || source === routing.chosenSessionId)
50
+ return undefined;
51
+ const connected = routing.connected();
52
+ if (!connected.some((s) => s.sessionId === source))
53
+ return undefined;
54
+ const listed = connected.map((s) => `'${s.sessionId}' (${s.url})`).join(', ');
55
+ return (`ref '${routing.ref}' was handed out by session '${source}', and this call named no sessionId so ` +
56
+ `it resolved to '${routing.chosenSessionId}'. A ref only means something in the tab it was taken ` +
57
+ 'in — refs are minted per document, so the same one can point at a different element in another ' +
58
+ 'tab — and Reticle refuses rather than guessing which tab you meant. Connected right now: ' +
59
+ `${listed}. Pass sessionId: '${source}' to act on the tab you looked at, or re-read the tab you ` +
60
+ 'actually want first. Nothing was driven.');
61
+ }
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * Any edit here is a per-session tax on every user: say it in as few words as still work.
10
10
  */
11
- const SESSION_LEASE_NOTICE = 'When you stop driving, call reticle_session {action:"yield", mode:"waiting"|"ask"} — the panel reads "live" until you do. {action:"end"} when the task is done; it revives on your next action.';
11
+ const SESSION_LEASE_NOTICE = 'When you stop driving, call reticle_session {action:"yield", mode:"waiting"|"ask"}. The panel reads "live" until you do. {action:"end"} when the task is done; it revives on your next action.';
12
12
  export function buildSessionLease(sessionId, openedAt) {
13
13
  return { sessionId, opened_at: openedAt, IMPORTANT: SESSION_LEASE_NOTICE };
14
14
  }
@@ -1,5 +1,6 @@
1
1
  import { Session, type SessionInfo } from './session.js';
2
2
  import { AttachmentHistory } from './attachment-history.js';
3
+ import type { NoSessionNextAction } from './no-session-next-action.js';
3
4
  /**
4
5
  * The agent's active project, used to scope auto-selection. `projectId` is the stable build-stamped
5
6
  * identity (authoritative when present); `url` is the app origin, a fallback hint for older SDKs that
@@ -15,6 +16,7 @@ export declare class SessionManager {
15
16
  #private;
16
17
  /** Set the active project's default resolve scope (called once at daemon wiring). */
17
18
  setDefaultScope(scope: ResolveScope | undefined): void;
19
+ setConnectionRecorder(record: ((projectId: string | undefined) => void) | undefined): void;
18
20
  add(session: Session): Session | undefined;
19
21
  remove(session: Session): boolean;
20
22
  get(sessionId: string): Session | undefined;
@@ -35,6 +37,8 @@ export declare class SessionManager {
35
37
  * already computed for the error path; this exposes it to the read path too.
36
38
  */
37
39
  noSessionHint(): string | undefined;
40
+ setNoSessionNextAction(next: (() => NoSessionNextAction | undefined) | undefined): void;
41
+ noSessionNextAction(): NoSessionNextAction | undefined;
38
42
  everConnected(): boolean;
39
43
  /** Record a bridge-initiated close so `resolve` can explain a session that vanished. */
40
44
  noteClosure(reason: string, at: number): void;
@@ -79,8 +79,20 @@ export class SessionManager {
79
79
  setDefaultScope(scope) {
80
80
  this.#defaultScope = scope;
81
81
  }
82
+ /**
83
+ * Told about every session that registers, so the fact outlives this process.
84
+ *
85
+ * Injected rather than done here: this class owns a registry, not the filesystem. Wired here
86
+ * because `add` is the ONE method every path that registers a session goes through — the same
87
+ * reasoning that put the redaction declaration below it.
88
+ */
89
+ #recordConnection;
90
+ setConnectionRecorder(record) {
91
+ this.#recordConnection = record;
92
+ }
82
93
  add(session) {
83
94
  this.#everConnected = true;
95
+ this.#recordConnection?.(session.projectId);
84
96
  const previous = this.#sessions.get(session.id);
85
97
  this.#sessions.set(session.id, session);
86
98
  // Publish what this app declared sensitive to the driven-path rule. Here rather than in the
@@ -172,6 +184,20 @@ export class SessionManager {
172
184
  noSessionHint() {
173
185
  return this.#noSessionHint?.();
174
186
  }
187
+ /**
188
+ * The same diagnosis with the prose taken out — one action kind and one literal command.
189
+ *
190
+ * Separate from the hint rather than folded into it because the two have different consumers and
191
+ * different lifetimes: `resolve` throws an Error, which can only ever carry a string, while
192
+ * `reticle_sessions` returns a structured payload an agent can branch on without parsing English.
193
+ */
194
+ #noSessionNextAction;
195
+ setNoSessionNextAction(next) {
196
+ this.#noSessionNextAction = next;
197
+ }
198
+ noSessionNextAction() {
199
+ return this.#noSessionNextAction?.();
200
+ }
175
201
  /** Whether any session has connected since this daemon booted — half the diagnosis. */
176
202
  #everConnected = false;
177
203
  everConnected() {
@@ -224,11 +250,15 @@ export class SessionManager {
224
250
  const closure = this.lastClosure();
225
251
  // A diagnosis beats a checklist: it names which of the three causes this actually is.
226
252
  const hint = this.#noSessionHint?.();
227
- if (hint !== undefined && closure === undefined)
228
- throw new Error(hint);
229
- throw new Error(closure === undefined
230
- ? NO_SESSION_CONNECTED_ERROR
231
- : `${NO_SESSION_CONNECTED_ERROR} NOTE: the bridge REFUSED or closed a connection recently — "${closure.reason}". The app is probably still running and trying to connect: it was turned away, and the SDK does not retry after a policy close. The reason above names the fix — do not go looking for a stopped dev server.`);
253
+ // BOTH, when there are both — the two answer different questions and the pair is strictly
254
+ // more than either. This used to be an either/or, and a recorded refusal DISCARDED the whole
255
+ // diagnosis: the one case where the daemon has hard evidence about why nothing is connected
256
+ // was also the one case where it threw away everything else it had learned, including the
257
+ // port scan and the next action. The refusal leads, because it is the fact.
258
+ const refusal = closure === undefined
259
+ ? ''
260
+ : ` NOTE: the bridge REFUSED or closed a connection recently — "${closure.reason}". The app is probably still running and trying to connect: it was turned away, and the SDK does not retry after a policy close. The reason above names the fix — do not go looking for a stopped dev server.`;
261
+ throw new Error(`${hint ?? NO_SESSION_CONNECTED_ERROR}${refusal}`);
232
262
  }
233
263
  // Scope to the agent's active project FIRST, so a stray tab from another app/origin (e.g. a
234
264
  // leftover dashboard on a different port) is structurally unselectable — it never enters the
@@ -1,4 +1,5 @@
1
1
  import type { WebSocket } from 'ws';
2
+ import type { ImpactSnapshot } from '@reticlehq/core';
2
3
  import { LastAct } from './last-act.js';
3
4
  import { type PageRuntime } from './command-timeout.js';
4
5
  import { type SessionHealth } from './session-health.js';
@@ -148,6 +149,7 @@ export declare class Session {
148
149
  handleResult(result: CommandResult): void;
149
150
  /** Reject everything still in flight — used on disconnect. */
150
151
  rejectAll(reason: string): void;
152
+ succeededBy(next: Session): void;
151
153
  /** End this transport without letting a stale socket remove its replacement session. */
152
154
  disconnect(reason: string): void;
153
155
  getState(): SessionState;
@@ -184,8 +186,15 @@ export declare class Session {
184
186
  * Push a lifecycle state to the panel with optional human-facing `text`. State changes still flow
185
187
  * through `setState`; an auto-ended session rides a `warn` tone so the panel can shout "agent stopped".
186
188
  */
189
+ /**
190
+ * Push the impact record to this tab's HUD.
191
+ *
192
+ * Debounced: the record changes on every tool call, and the panel does not need 200 frames to
193
+ * show a counter going up. The LAST state always lands, because the timer re-reads the store
194
+ * when it fires rather than capturing a snapshot when it was scheduled.
195
+ */
196
+ pushImpact(read: () => ImpactSnapshot | undefined, immediate?: boolean): void;
187
197
  pushPresenter(state: SessionState, text?: string, tone?: PresenterTone): void;
188
- /** Fire-and-forget a narration row to the live panel (so a resolved mark shows "✓ fixed"). */
189
198
  pushNarration(text: string): void;
190
199
  /** The one-time lease block on the first agent command, then undefined forever after. */
191
200
  takeSessionLease(): SessionLease | undefined;