@reticlehq/server 2.12.0 → 2.13.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 (386) hide show
  1. package/SKILL.md +67 -109
  2. package/dist/bridge/auth-failure-reason.d.ts +6 -2
  3. package/dist/bridge/auth-failure-reason.js +15 -8
  4. package/dist/bridge/bridge.d.ts +18 -1
  5. package/dist/bridge/bridge.js +247 -164
  6. package/dist/bridge/pairing-token.d.ts +5 -0
  7. package/dist/bridge/pairing-token.js +28 -0
  8. package/dist/bridge/protocol-skew.d.ts +37 -0
  9. package/dist/bridge/protocol-skew.js +45 -0
  10. package/dist/capsule/capsule-store.d.ts +6 -6
  11. package/dist/capsule/causal-summary.js +7 -2
  12. package/dist/cli/cli-doctor.js +31 -1
  13. package/dist/cli/cli-launch.d.ts +11 -1
  14. package/dist/cli/cli-launch.js +59 -10
  15. package/dist/cli/cli-parse.d.ts +15 -1
  16. package/dist/cli/cli-parse.js +212 -8
  17. package/dist/cli/cli-verify.d.ts +1 -0
  18. package/dist/cli/cli-verify.js +3 -2
  19. package/dist/cli/cloud-cli.js +428 -259
  20. package/dist/cli/cloud-keystore.d.ts +37 -0
  21. package/dist/cli/cloud-keystore.js +97 -0
  22. package/dist/cli/cloud-kit.d.ts +37 -0
  23. package/dist/cli/cloud-kit.js +121 -0
  24. package/dist/cli/cloud-login.d.ts +17 -0
  25. package/dist/cli/cloud-login.js +183 -0
  26. package/dist/cli/cloud-session.d.ts +48 -0
  27. package/dist/cli/cloud-session.js +53 -0
  28. package/dist/cli/config-discovery.js +1 -0
  29. package/dist/cli/daemon-start-options.d.ts +32 -0
  30. package/dist/cli/daemon-start-options.js +38 -0
  31. package/dist/cli/device-flow.d.ts +76 -0
  32. package/dist/cli/device-flow.js +56 -0
  33. package/dist/cli/doctor-rows.d.ts +9 -0
  34. package/dist/cli/doctor-rows.js +9 -0
  35. package/dist/cli/drive-attach.d.ts +13 -0
  36. package/dist/cli/drive-attach.js +18 -0
  37. package/dist/cli/drive-command.js +7 -3
  38. package/dist/cli/mcp-command.js +51 -20
  39. package/dist/cli/project-name.d.ts +30 -0
  40. package/dist/cli/project-name.js +44 -0
  41. package/dist/cli/status-next-action.d.ts +14 -0
  42. package/dist/cli/status-next-action.js +4 -4
  43. package/dist/cli.d.ts +1 -0
  44. package/dist/cli.js +134 -80
  45. package/dist/cloud/cloud-config.d.ts +22 -1
  46. package/dist/cloud/cloud-config.js +64 -5
  47. package/dist/cloud/cloud-sync.d.ts +5 -5
  48. package/dist/cloud/cloud-sync.js +33 -2
  49. package/dist/cloud/sync-cycle.d.ts +110 -0
  50. package/dist/cloud/sync-cycle.js +270 -0
  51. package/dist/cloud/sync-daemon.d.ts +69 -0
  52. package/dist/cloud/sync-daemon.js +260 -0
  53. package/dist/cloud/sync-disk.d.ts +9 -0
  54. package/dist/cloud/sync-disk.js +191 -0
  55. package/dist/cloud/sync-hash.d.ts +2 -0
  56. package/dist/cloud/sync-hash.js +35 -0
  57. package/dist/crawl/crawl-tools.js +5 -3
  58. package/dist/crawl/crawl.d.ts +3 -0
  59. package/dist/crawl/crawl.js +5 -0
  60. package/dist/daemon/daemon-resolve.d.ts +43 -0
  61. package/dist/daemon/daemon-resolve.js +69 -0
  62. package/dist/daemon/dev-servers.d.ts +15 -0
  63. package/dist/daemon/dev-servers.js +68 -0
  64. package/dist/daemon/wake-decision.d.ts +32 -0
  65. package/dist/daemon/wake-decision.js +38 -0
  66. package/dist/domain/domain-tools.js +2 -2
  67. package/dist/events/contradictions.d.ts +18 -0
  68. package/dist/events/contradictions.js +134 -21
  69. package/dist/events/declared.d.ts +7 -4
  70. package/dist/events/declared.js +15 -6
  71. package/dist/events/echo-mismatch.js +64 -14
  72. package/dist/events/event-filters.d.ts +8 -0
  73. package/dist/events/event-filters.js +19 -2
  74. package/dist/events/predicate-eval.js +53 -5
  75. package/dist/events/predicate-precheck.d.ts +8 -0
  76. package/dist/events/predicate-precheck.js +41 -0
  77. package/dist/events/predicate-route.d.ts +14 -0
  78. package/dist/events/predicate-route.js +36 -8
  79. package/dist/events/predicate.d.ts +5 -0
  80. package/dist/events/predicate.js +37 -5
  81. package/dist/events/split-text-miss.d.ts +30 -0
  82. package/dist/events/split-text-miss.js +44 -0
  83. package/dist/flows/flake.d.ts +6 -6
  84. package/dist/flows/flow-memory-consult.d.ts +55 -0
  85. package/dist/flows/flow-memory-consult.js +38 -0
  86. package/dist/flows/flow-replay-run.d.ts +18 -7
  87. package/dist/flows/flow-replay-run.js +239 -22
  88. package/dist/flows/flow-replay.js +11 -2
  89. package/dist/flows/flow-step-runners.d.ts +8 -0
  90. package/dist/flows/flow-step-runners.js +8 -2
  91. package/dist/flows/flow-store-for-session.d.ts +31 -0
  92. package/dist/flows/flow-store-for-session.js +33 -0
  93. package/dist/flows/flow-tools.js +58 -32
  94. package/dist/flows/flows.d.ts +8 -0
  95. package/dist/flows/flows.js +468 -2
  96. package/dist/flows/heal-run.js +5 -2
  97. package/dist/flows/predicate-to-expect.d.ts +17 -12
  98. package/dist/flows/predicate-to-expect.js +30 -12
  99. package/dist/flows/recordings.d.ts +13 -1
  100. package/dist/flows/recordings.js +17 -2
  101. package/dist/flows/replay.d.ts +22 -2
  102. package/dist/flows/replay.js +28 -2
  103. package/dist/flows/verify-change-tools.js +5 -2
  104. package/dist/honesty/blind-spots.d.ts +55 -2
  105. package/dist/honesty/blind-spots.js +63 -3
  106. package/dist/honesty/honesty.d.ts +24 -0
  107. package/dist/honesty/honesty.js +25 -1
  108. package/dist/honesty/instrumentation-gaps.d.ts +9 -0
  109. package/dist/honesty/instrumentation-gaps.js +29 -2
  110. package/dist/honesty/verified.d.ts +2 -1
  111. package/dist/honesty/verified.js +16 -1
  112. package/dist/http-server.js +12 -0
  113. package/dist/impact/impact-recorder.d.ts +31 -8
  114. package/dist/impact/impact-recorder.js +104 -21
  115. package/dist/impact/impact-store.d.ts +15 -1
  116. package/dist/impact/impact-store.js +43 -2
  117. package/dist/index.js +113 -7
  118. package/dist/init/agent-rules.d.ts +13 -6
  119. package/dist/init/agent-rules.js +23 -12
  120. package/dist/init/astro-patch.d.ts +16 -0
  121. package/dist/init/astro-patch.js +28 -0
  122. package/dist/init/capabilities.d.ts +4 -1
  123. package/dist/init/capabilities.js +22 -2
  124. package/dist/init/confirm.d.ts +31 -7
  125. package/dist/init/confirm.js +48 -8
  126. package/dist/init/cra.d.ts +20 -4
  127. package/dist/init/cra.js +57 -17
  128. package/dist/init/csp-check.js +17 -2
  129. package/dist/init/csp-doctor.d.ts +8 -1
  130. package/dist/init/csp-doctor.js +8 -3
  131. package/dist/init/dev-script.d.ts +3 -1
  132. package/dist/init/dev-script.js +8 -5
  133. package/dist/init/format-generated.d.ts +18 -0
  134. package/dist/init/format-generated.js +39 -0
  135. package/dist/init/install-hint.d.ts +9 -0
  136. package/dist/init/install-hint.js +29 -0
  137. package/dist/init/mcp-clients.d.ts +7 -0
  138. package/dist/init/mcp-clients.js +7 -0
  139. package/dist/init/mcp.d.ts +1 -1
  140. package/dist/init/mcp.js +1 -1
  141. package/dist/init/node-io.js +11 -1
  142. package/dist/init/plan-framework.js +81 -45
  143. package/dist/init/plan.d.ts +20 -7
  144. package/dist/init/plan.js +20 -45
  145. package/dist/init/preflight.d.ts +28 -0
  146. package/dist/init/preflight.js +38 -0
  147. package/dist/init/run.d.ts +32 -9
  148. package/dist/init/run.js +141 -119
  149. package/dist/init/slash-command.d.ts +1 -1
  150. package/dist/init/snippets.js +41 -25
  151. package/dist/init/vite-config.d.ts +1 -1
  152. package/dist/init/vite-config.js +39 -25
  153. package/dist/init/workspace-apps.d.ts +17 -10
  154. package/dist/init/workspace-apps.js +110 -0
  155. package/dist/input/network-mock-tools.js +3 -6
  156. package/dist/input/real-input.js +25 -5
  157. package/dist/input/scroll-tools.js +4 -6
  158. package/dist/input/viewport-tools.js +5 -4
  159. package/dist/intent/inline-intent.d.ts +10 -19
  160. package/dist/intent/inline-intent.js +91 -1
  161. package/dist/intent/intent-shard-store.d.ts +72 -0
  162. package/dist/intent/intent-shard-store.js +203 -0
  163. package/dist/intent/intent-shard.d.ts +411 -0
  164. package/dist/intent/intent-shard.js +176 -0
  165. package/dist/intent/intent-store.d.ts +12 -0
  166. package/dist/intent/intent-store.js +21 -0
  167. package/dist/intent/intent-subject.d.ts +45 -0
  168. package/dist/intent/intent-subject.js +177 -0
  169. package/dist/intent/intent-tools.js +65 -2
  170. package/dist/intent/open-intents.d.ts +21 -0
  171. package/dist/intent/open-intents.js +15 -0
  172. package/dist/license/license-env.d.ts +29 -0
  173. package/dist/license/license-env.js +148 -0
  174. package/dist/loopback-agent.d.ts +31 -0
  175. package/dist/loopback-agent.js +57 -0
  176. package/dist/mcp/attach-memory.d.ts +18 -2
  177. package/dist/mcp/attach-memory.js +50 -7
  178. package/dist/mcp/client-identity.d.ts +31 -0
  179. package/dist/mcp/client-identity.js +51 -0
  180. package/dist/mcp/mcp-post-transport.d.ts +26 -0
  181. package/dist/mcp/mcp-post-transport.js +109 -0
  182. package/dist/mcp/mcp-proxy.d.ts +2 -1
  183. package/dist/mcp/mcp-proxy.js +52 -62
  184. package/dist/mcp/mcp.js +7 -1
  185. package/dist/mcp/proxy-backoff.d.ts +11 -0
  186. package/dist/mcp/proxy-backoff.js +13 -0
  187. package/dist/mcp/proxy-daemon-probe.d.ts +1 -0
  188. package/dist/mcp/proxy-daemon-probe.js +7 -1
  189. package/dist/mcp/proxy-handshake.d.ts +23 -0
  190. package/dist/mcp/proxy-handshake.js +31 -0
  191. package/dist/mcp/tool-catalog-cache.d.ts +8 -0
  192. package/dist/mcp/tool-catalog-cache.js +12 -0
  193. package/dist/memory/memory-tools.d.ts +2 -0
  194. package/dist/memory/memory-tools.js +82 -0
  195. package/dist/memory/project-memory.d.ts +34 -0
  196. package/dist/memory/project-memory.js +96 -0
  197. package/dist/oracles/propose-consequences.js +12 -3
  198. package/dist/pool/browser-pool.d.ts +63 -4
  199. package/dist/pool/browser-pool.js +113 -4
  200. package/dist/pool/playwright-launcher.js +9 -0
  201. package/dist/project/project-tools.js +3 -3
  202. package/dist/project/reticle-dir.d.ts +15 -2
  203. package/dist/project/reticle-dir.js +21 -4
  204. package/dist/runs/run-store.d.ts +10 -0
  205. package/dist/runs/run-store.js +10 -0
  206. package/dist/runs/verification-sync.d.ts +0 -11
  207. package/dist/runs/verification-sync.js +22 -1
  208. package/dist/runs/verify-server.d.ts +8 -1
  209. package/dist/runs/verify-server.js +14 -2
  210. package/dist/session/command-timeout.d.ts +5 -2
  211. package/dist/session/command-timeout.js +5 -4
  212. package/dist/session/lease-visibility.d.ts +67 -0
  213. package/dist/session/lease-visibility.js +76 -0
  214. package/dist/session/no-session-diagnosis.d.ts +23 -0
  215. package/dist/session/no-session-diagnosis.js +58 -17
  216. package/dist/session/no-session-next-action.d.ts +7 -0
  217. package/dist/session/no-session-next-action.js +42 -3
  218. package/dist/session/no-session-watch.d.ts +10 -6
  219. package/dist/session/no-session-watch.js +51 -19
  220. package/dist/session/pending-commands.d.ts +11 -1
  221. package/dist/session/pending-commands.js +12 -2
  222. package/dist/session/resolve-within.d.ts +34 -0
  223. package/dist/session/resolve-within.js +53 -0
  224. package/dist/session/session-info.d.ts +18 -0
  225. package/dist/session/session-info.js +11 -0
  226. package/dist/session/session-manager.d.ts +14 -0
  227. package/dist/session/session-manager.js +61 -3
  228. package/dist/session/session-mirror.d.ts +16 -0
  229. package/dist/session/session-mirror.js +22 -0
  230. package/dist/session/session-recommendation.d.ts +19 -5
  231. package/dist/session/session-recommendation.js +25 -9
  232. package/dist/session/session-successor.js +36 -1
  233. package/dist/session/session-tools.js +2 -2
  234. package/dist/session/session.d.ts +22 -2
  235. package/dist/session/session.js +87 -7
  236. package/dist/session/wall-clock.d.ts +12 -0
  237. package/dist/session/wall-clock.js +14 -0
  238. package/dist/setup/agent-configs.d.ts +96 -0
  239. package/dist/setup/agent-configs.js +333 -0
  240. package/dist/setup/agent-io.d.ts +9 -0
  241. package/dist/setup/agent-io.js +16 -0
  242. package/dist/setup/agent-writer.d.ts +51 -0
  243. package/dist/setup/agent-writer.js +115 -0
  244. package/dist/setup/approval-migration.d.ts +47 -0
  245. package/dist/setup/approval-migration.js +68 -0
  246. package/dist/setup/auto-approve.d.ts +85 -0
  247. package/dist/setup/auto-approve.js +150 -0
  248. package/dist/setup/bridge-port.d.ts +15 -0
  249. package/dist/setup/bridge-port.js +24 -0
  250. package/dist/setup/desktop-shape.d.ts +57 -0
  251. package/dist/setup/desktop-shape.js +63 -0
  252. package/dist/setup/dev-server-wait.d.ts +60 -0
  253. package/dist/setup/dev-server-wait.js +65 -0
  254. package/dist/setup/drive-agent.d.ts +71 -0
  255. package/dist/setup/drive-agent.js +171 -0
  256. package/dist/setup/drive-plan.d.ts +68 -0
  257. package/dist/setup/drive-plan.js +130 -0
  258. package/dist/setup/ensure-daemon.d.ts +29 -0
  259. package/dist/setup/ensure-daemon.js +58 -0
  260. package/dist/setup/init-runtime.d.ts +47 -0
  261. package/dist/setup/init-runtime.js +160 -0
  262. package/dist/setup/interrupt.d.ts +25 -0
  263. package/dist/setup/interrupt.js +40 -0
  264. package/dist/setup/license-key.d.ts +39 -0
  265. package/dist/setup/license-key.js +72 -0
  266. package/dist/setup/listeners.d.ts +37 -0
  267. package/dist/setup/listeners.js +85 -0
  268. package/dist/setup/node-effects.d.ts +60 -0
  269. package/dist/setup/node-effects.js +210 -0
  270. package/dist/setup/page-probe.d.ts +39 -0
  271. package/dist/setup/page-probe.js +49 -0
  272. package/dist/setup/relaunch.d.ts +36 -0
  273. package/dist/setup/relaunch.js +57 -0
  274. package/dist/setup/remaining-steps.d.ts +19 -0
  275. package/dist/setup/remaining-steps.js +31 -0
  276. package/dist/setup/run-setup.d.ts +99 -0
  277. package/dist/setup/run-setup.js +187 -0
  278. package/dist/setup/session-pick.d.ts +47 -0
  279. package/dist/setup/session-pick.js +51 -0
  280. package/dist/setup/setup-command.d.ts +45 -0
  281. package/dist/setup/setup-command.js +199 -0
  282. package/dist/setup/setup-options.d.ts +74 -0
  283. package/dist/setup/setup-options.js +67 -0
  284. package/dist/setup/transcripts.d.ts +16 -0
  285. package/dist/setup/transcripts.js +43 -0
  286. package/dist/status-payload.d.ts +14 -1
  287. package/dist/status-payload.js +23 -1
  288. package/dist/telemetry/daemon-telemetry.d.ts +8 -1
  289. package/dist/telemetry/daemon-telemetry.js +52 -5
  290. package/dist/telemetry/error-class.js +1 -1
  291. package/dist/telemetry/feedback-context.d.ts +9 -1
  292. package/dist/telemetry/feedback-context.js +60 -29
  293. package/dist/telemetry/feedback-local.d.ts +15 -0
  294. package/dist/telemetry/feedback-local.js +56 -0
  295. package/dist/telemetry/feedback.d.ts +11 -0
  296. package/dist/telemetry/feedback.js +19 -6
  297. package/dist/telemetry/init-telemetry.d.ts +2 -0
  298. package/dist/telemetry/init-telemetry.js +2 -0
  299. package/dist/telemetry/install-source.d.ts +14 -0
  300. package/dist/telemetry/install-source.js +30 -0
  301. package/dist/telemetry/license-activation.d.ts +7 -1
  302. package/dist/telemetry/license-activation.js +9 -5
  303. package/dist/telemetry/mcp-connection.js +7 -0
  304. package/dist/telemetry/project-profile.d.ts +12 -1
  305. package/dist/telemetry/project-profile.js +11 -6
  306. package/dist/telemetry/proxy-telemetry.d.ts +3 -0
  307. package/dist/telemetry/proxy-telemetry.js +21 -0
  308. package/dist/telemetry/session-metrics.d.ts +12 -0
  309. package/dist/telemetry/session-metrics.js +35 -1
  310. package/dist/telemetry/telemetry.js +17 -2
  311. package/dist/telemetry/tool-refused.d.ts +3 -1
  312. package/dist/telemetry/tool-refused.js +34 -2
  313. package/dist/tools/act-capsule.d.ts +10 -0
  314. package/dist/tools/act-capsule.js +8 -2
  315. package/dist/tools/act-danger.d.ts +2 -1
  316. package/dist/tools/act-danger.js +11 -3
  317. package/dist/tools/act-observation.d.ts +41 -0
  318. package/dist/tools/act-observation.js +36 -0
  319. package/dist/tools/act-preflight.d.ts +20 -0
  320. package/dist/tools/act-preflight.js +98 -0
  321. package/dist/tools/act-sequence-retry.d.ts +45 -0
  322. package/dist/tools/act-sequence-retry.js +58 -0
  323. package/dist/tools/act-sequence-tool.d.ts +13 -0
  324. package/dist/tools/act-sequence-tool.js +135 -0
  325. package/dist/tools/act-target.d.ts +14 -0
  326. package/dist/tools/act-target.js +47 -0
  327. package/dist/tools/act-tools.d.ts +21 -1
  328. package/dist/tools/act-tools.js +100 -213
  329. package/dist/tools/assert-verdict.js +22 -16
  330. package/dist/tools/browser-tools.js +6 -2
  331. package/dist/tools/dynamic-tools.js +24 -5
  332. package/dist/tools/error-recovery.d.ts +1 -0
  333. package/dist/tools/error-recovery.js +8 -0
  334. package/dist/tools/invoke-tool.js +59 -9
  335. package/dist/tools/lease-tools.d.ts +30 -4
  336. package/dist/tools/lease-tools.js +169 -5
  337. package/dist/tools/merged-name-redirect.d.ts +13 -0
  338. package/dist/tools/merged-name-redirect.js +22 -0
  339. package/dist/tools/numeric-bounds.d.ts +100 -0
  340. package/dist/tools/numeric-bounds.js +125 -0
  341. package/dist/tools/observe-tools.js +47 -40
  342. package/dist/tools/read-tools.js +28 -7
  343. package/dist/tools/real-input-attempt.d.ts +10 -0
  344. package/dist/tools/real-input-attempt.js +59 -1
  345. package/dist/tools/reconcile-tools.js +4 -4
  346. package/dist/tools/tool-kit.d.ts +12 -0
  347. package/dist/tools/tool-kit.js +6 -1
  348. package/dist/tools/tool-names.d.ts +2 -0
  349. package/dist/tools/tool-names.js +2 -0
  350. package/dist/tools/tools-helpers.d.ts +19 -0
  351. package/dist/tools/tools-helpers.js +46 -0
  352. package/dist/tools/tools.d.ts +2 -0
  353. package/dist/tools/tools.js +56 -8
  354. package/dist/update/reticle-deps.d.ts +9 -0
  355. package/dist/update/reticle-deps.js +27 -0
  356. package/dist/update/sdk-sync.d.ts +1 -2
  357. package/dist/update/sdk-sync.js +1 -21
  358. package/dist/update/update-nudge.d.ts +18 -0
  359. package/dist/update/update-nudge.js +20 -0
  360. package/dist/version/peer-announce.js +4 -2
  361. package/dist/version/sdk-fix.d.ts +47 -0
  362. package/dist/version/sdk-fix.js +127 -0
  363. package/dist/version/version-nudge.d.ts +14 -0
  364. package/dist/version/version-nudge.js +21 -0
  365. package/dist/version/version-skew.d.ts +38 -3
  366. package/dist/version/version-skew.js +83 -7
  367. package/dist/visual/visual-store.d.ts +6 -6
  368. package/dist/visual/visual-store.js +18 -16
  369. package/dist/visual/visual-tools.js +57 -12
  370. package/docs/cli/doctor.mdx +1 -0
  371. package/docs/cli/serve.mdx +4 -2
  372. package/docs/cli.mdx +7 -3
  373. package/docs/desktop.mdx +1 -1
  374. package/docs/docs.json +1 -1
  375. package/docs/getting-started.md +19 -3
  376. package/docs/index.mdx +9 -6
  377. package/docs/install-agentic.mdx +38 -12
  378. package/docs/install-manual.mdx +1 -1
  379. package/docs/packages.mdx +1 -1
  380. package/docs/skill-file.mdx +3 -3
  381. package/docs/telemetry-contract.md +108 -4
  382. package/docs/tools-act-sequence.mdx +16 -0
  383. package/docs/usage.md +1 -1
  384. package/package.json +4 -3
  385. package/dist/init/cursor.d.ts +0 -24
  386. package/dist/init/cursor.js +0 -80
@@ -1,7 +1,7 @@
1
1
  import * as http from 'node:http';
2
- import { localInitializeResponse, isHandshakeLine, drainLines, MAX_STDIN_LINE_BYTES, } from './proxy-handshake.js';
3
- import { isToolsListRequest, ToolCatalogCache } from './tool-catalog-cache.js';
4
- import { rememberEnumerated, rememberProxyStarted } from './attach-memory.js';
2
+ import { localInitializeResponse, degradedInstructions, isHandshakeLine, drainLines, MAX_STDIN_LINE_BYTES, } from './proxy-handshake.js';
3
+ import { isToolCallRequest, isToolsListRequest, ToolCatalogCache } from './tool-catalog-cache.js';
4
+ import { rememberEnumerated, rememberProxyStarted, rememberToolCalled } from './attach-memory.js';
5
5
  import { toolsChangedNotification } from './proxy-handshake.js';
6
6
  import { LOOPBACK_HOST, MCP_SSE_PATH, MCP_SHUTDOWN_EVENT, ReticleEnv, CONTRACT_FINGERPRINT, } from '@reticlehq/core';
7
7
  import { SseFrameParser } from './sse-frame-parser.js';
@@ -16,6 +16,7 @@ import { readProjectId } from '../cli/cli-port.js';
16
16
  import { PEER_VERSION_PARAM, PEER_CONTRACT_PARAM } from '../version/peer-announce.js';
17
17
  import { onStreamDrop, onClientRequest, onReconnectBudgetSpent, onQueueExpired, OnDrop, OnRequest, } from './proxy-lifecycle.js';
18
18
  import { describePresence, probePresence } from '../daemon/port-presence.js';
19
+ import { flushProxySessionMetrics } from '../telemetry/proxy-telemetry.js';
19
20
  /**
20
21
  * The same `/status` probe `doctor`, `status` and `kill` ask with. Reused rather than re-written:
21
22
  * the whole defect this import closes was the proxy answering a DIFFERENT question from every other
@@ -28,11 +29,11 @@ import { fetchStatus as fetchDaemonStatus } from '../cli/cli-launch.js';
28
29
  */
29
30
  export { accountProxyLogWrite, proxyLog, proxyLogPath, PROXY_LOG_CHECK_BYTES, recoverOversizedProxyLog, setProxyLogPort, } from './proxy-log.js';
30
31
  import { proxyLog } from './proxy-log.js';
31
- import { log } from '../log.js';
32
32
  import { OutageReason, OutageStage, reportMcpOutage } from './mcp-outage.js';
33
- /** Reconnect backoff: linear, capped, so a briefly-restarting daemon is picked up fast. */
34
- const RECONNECT_BASE_MS = 250;
35
- const RECONNECT_CAP_MS = 5_000;
33
+ import { postToSession } from './mcp-post-transport.js';
34
+ import { reconnectDelayMs } from './proxy-backoff.js';
35
+ export { reconnectDelayMs, RECONNECT_BASE_MS, RECONNECT_CAP_MS } from './proxy-backoff.js';
36
+ export { MCP_PROXY_HTTP_AGENT_OPTIONS, postToSession, shouldRetryUnsentPost, } from './mcp-post-transport.js';
36
37
  /**
37
38
  * How many consecutive failed reconnects before the proxy stops RETRYING. It does not stop serving:
38
39
  * the budget ends the retry loop and the proxy goes dormant, where the handshake and `tools/list`
@@ -51,9 +52,6 @@ export const MAX_RECONNECT_ATTEMPTS = (() => {
51
52
  const raw = Number(process.env[ReticleEnv.RECONNECT_ATTEMPTS]);
52
53
  return Number.isInteger(raw) && raw > 0 ? raw : DEFAULT_MAX_RECONNECT_ATTEMPTS;
53
54
  })();
54
- export function reconnectDelayMs(attempt) {
55
- return Math.min(RECONNECT_BASE_MS * attempt, RECONNECT_CAP_MS);
56
- }
57
55
  /** JSON-RPC id the proxy uses for its own replayed `initialize` — never one the client could send. */
58
56
  export const RECONNECT_INITIALIZE_ID = '__reticle_proxy_reinit';
59
57
  function parseJsonRpc(line) {
@@ -254,49 +252,6 @@ export class HandshakeReplay {
254
252
  return true;
255
253
  }
256
254
  }
257
- /**
258
- * POST one JSON-RPC line into the daemon's session. Resolves null on success, or a short reason
259
- * when the request never reached a server that will answer it.
260
- *
261
- * It used to resolve `void` in every case, logging the failure and moving on. That is the THIRD way
262
- * a call goes unanswered, and the only one neither `streamLossReplies` nor the queue timer can see:
263
- * the SSE stream is healthy (so nothing drops) and the request was forwarded (so nothing is queued),
264
- * but the POST leg is its own TCP connection and an ECONNRESET on it means the daemon never received
265
- * the call. Nobody was ever going to reply, and the caller waited for its own timeout.
266
- */
267
- function postToSession(url, body) {
268
- return new Promise((resolve) => {
269
- const parsed = new URL(url);
270
- const bodyBuf = Buffer.from(body, 'utf8');
271
- const options = {
272
- host: parsed.hostname,
273
- port: parsed.port !== '' ? parseInt(parsed.port, 10) : 80,
274
- path: `${parsed.pathname}${parsed.search}`,
275
- method: 'POST',
276
- headers: {
277
- 'Content-Type': 'application/json',
278
- 'Content-Length': bodyBuf.byteLength,
279
- },
280
- };
281
- const req = http.request(options, (res) => {
282
- const status = res.statusCode ?? 0;
283
- const rejected = status < 200 || status >= 300;
284
- if (rejected) {
285
- // A non-2xx from the daemon MCP endpoint used to be swallowed, hanging the JSON-RPC call
286
- // client-side with no diagnostic. It is a refusal: no response is coming over the stream.
287
- log('reticle_mcp_proxy_post_non2xx', { status, path: options.path });
288
- }
289
- res.resume(); // drain so the socket is reused
290
- resolve(rejected ? `daemon rejected the call with HTTP ${String(status)}` : null);
291
- });
292
- req.on('error', (err) => {
293
- log('reticle_mcp_proxy_post_error', { error: err.message });
294
- resolve(`post failed: ${err.message}`);
295
- });
296
- req.write(bodyBuf);
297
- req.end();
298
- });
299
- }
300
255
  /**
301
256
  * Turn an `endpoint` frame's data into a POST target, or null when there is nothing usable in it.
302
257
  *
@@ -439,6 +394,17 @@ ensureDaemon) {
439
394
  let daemonRetiring = false;
440
395
  /** Why the stream we are currently trying to replace went away — carried onto the recovery. */
441
396
  let lastDropReason = OutageReason.OTHER;
397
+ /**
398
+ * What the outage had already cost when the proxy went dormant.
399
+ *
400
+ * The wake path clears `attempts` before dialling, and it must: a wake is a fresh retry budget,
401
+ * and sharing the previous outage's spent budget would put the proxy dormant again on its first
402
+ * failure. But the recovery report is guarded on `attempts > 0`, so clearing it also erased the
403
+ * evidence that there was anything to recover FROM — and dormancy is the commonest outage class
404
+ * we have, which made every recovery of that class invisible. Carried across the wake so the
405
+ * budget resets and the accounting does not.
406
+ */
407
+ let attemptsBeforeDormant = 0;
442
408
  /**
443
409
  * When the proxy started, and how many times the first connect has been refused since.
444
410
  *
@@ -461,13 +427,20 @@ ensureDaemon) {
461
427
  const msg = parseJsonRpc(line);
462
428
  if (null === msg || msg.id === undefined || !pending.take(msg.id))
463
429
  return;
430
+ if (failure.transport) {
431
+ reportMcpOutage(OutageStage.FIRST, {
432
+ reason: OutageReason.CONNECT_ERROR,
433
+ attempts: failure.attempts,
434
+ pendingLost: 1,
435
+ });
436
+ }
464
437
  proxyLog('reticle_mcp_proxy_post_unanswered', {
465
438
  port,
466
439
  method: String(msg.method),
467
- reason: failure,
440
+ reason: failure.reason,
468
441
  note: 'the stream is still up, so nothing else would ever have answered this call',
469
442
  });
470
- emit(transportLossReply(msg.id, failure));
443
+ emit(transportLossReply(msg.id, failure.reason));
471
444
  });
472
445
  };
473
446
  function onSseEvent(event, data, p) {
@@ -487,9 +460,11 @@ ensureDaemon) {
487
460
  // off and never reaching the budget that puts the proxy dormant. Measured: 32 retries in 8s.
488
461
  // Report the recovery BEFORE the counter is cleared: `attempts` is what coming back actually
489
462
  // cost, and it is the only number that can falsify the drop event. See OutageStage.RECOVERED.
490
- if (attempts > 0)
491
- reportMcpOutage(OutageStage.RECOVERED, { reason: lastDropReason, attempts });
463
+ const cost = attempts + attemptsBeforeDormant;
464
+ if (cost > 0)
465
+ reportMcpOutage(OutageStage.RECOVERED, { reason: lastDropReason, attempts: cost });
492
466
  attempts = 0;
467
+ attemptsBeforeDormant = 0;
493
468
  // The new session's McpServer has never seen the client's initialize — replay it first, then
494
469
  // flush whatever the client sent while we were reconnecting.
495
470
  for (const line of replay.replayLines())
@@ -710,10 +685,10 @@ ensureDaemon) {
710
685
  const armLocalHandshake = (line) => {
711
686
  if (handshakeAnswered)
712
687
  return;
713
- // Ask the same question the daemon would, from the same durable memory, so the client gets
714
- // the same answer it would have got had a daemon been up to give it.
715
- const response = localInitializeResponse(line, proxyInstructions(port));
716
- if (null === response)
688
+ // Only an `initialize` REQUEST is ours to answer. Asked with no instructions because the ones
689
+ // we will actually send name the reason the daemon failed, and the reason is not known until
690
+ // the deliver closure runs — see degradedInstructions.
691
+ if (null === localInitializeResponse(line, ''))
717
692
  return;
718
693
  // Take the debt NOW, not when the timer fires.
719
694
  //
@@ -732,6 +707,13 @@ ensureDaemon) {
732
707
  deliverHandshakeLocally = (reason) => {
733
708
  if (handshakeAnswered || postUrl !== null)
734
709
  return;
710
+ // Built here, not at arm time, so the notice can name WHY no daemon answered. The client
711
+ // reads `instructions` exactly once, at initialize, and this response is the only one it
712
+ // will ever get — so a handshake that does not say it is unbacked never says it at all.
713
+ // See degradedInstructions for the field report this closes.
714
+ const response = localInitializeResponse(line, degradedInstructions(proxyInstructions(port), port, reason));
715
+ if (null === response)
716
+ return;
735
717
  handshakeAnswered = true;
736
718
  deliverHandshakeLocally = undefined;
737
719
  // The catalog we are about to serve is ours, not the daemon's, so it has to be corrected
@@ -779,6 +761,11 @@ ensureDaemon) {
779
761
  // that never asks look identical to one that asked and was refused.
780
762
  if (isToolsListRequest(trimmed))
781
763
  rememberEnumerated(reticleStateHome(), port);
764
+ // The hop every other check misses. Recorded on the REQUEST rather than a reply, because
765
+ // the question is whether anything ever left the agent — a call that leaves and gets no
766
+ // answer is a different fault from one that was never made, and only this side sees it.
767
+ if (isToolCallRequest(trimmed))
768
+ rememberToolCalled(reticleStateHome(), port);
782
769
  const action = onClientRequest(postUrl !== null, dormant);
783
770
  if (action === OnRequest.SEND && postUrl !== null) {
784
771
  forward(postUrl, trimmed);
@@ -805,6 +792,8 @@ ensureDaemon) {
805
792
  // flushed once the new session's `endpoint` frame arrives.
806
793
  if (action === OnRequest.WAKE) {
807
794
  dormant = false;
795
+ // Budget reset, accounting preserved. See attemptsBeforeDormant.
796
+ attemptsBeforeDormant += attempts;
808
797
  attempts = 0;
809
798
  void (ensureDaemon?.() ?? Promise.resolve())
810
799
  .then(() => connect(false))
@@ -832,7 +821,8 @@ ensureDaemon) {
832
821
  process.stdin.on('end', () => {
833
822
  const quit = (code) => {
834
823
  stopped = true;
835
- process.exit(code);
824
+ // Await: fire-and-forget here is how daemon_stopped never arrived.
825
+ void flushProxySessionMetrics().finally(() => process.exit(code));
836
826
  };
837
827
  if (0 === pending.unanswered.length) {
838
828
  quit(0);
package/dist/mcp/mcp.js CHANGED
@@ -11,6 +11,7 @@ import { buildDynamicTools } from '../tools/dynamic-tools.js';
11
11
  import { runTool, SESSION_BOUND_TOOLS } from '../tools/invoke-tool.js';
12
12
  import { sessionEnvelopeShape } from '../tools/tool-kit.js';
13
13
  import { buildErrorPayload } from '../tools/error-recovery.js';
14
+ import { takeVersionSkewOnto } from '../version/version-nudge.js';
14
15
  import { resultIsError } from './mcp-is-error.js';
15
16
  import { buildServerInstructions } from './server-instructions.js';
16
17
  import { unadvertisedToolHelp } from '../tools/unadvertised-help.js';
@@ -514,7 +515,12 @@ tools = TOOLS) {
514
515
  // Every error the agent hits should answer "what next?", not just "what broke".
515
516
  return {
516
517
  isError: true,
517
- content: [{ type: 'text', text: JSON.stringify(buildErrorPayload(message)) }],
518
+ content: [
519
+ {
520
+ type: 'text',
521
+ text: JSON.stringify(takeVersionSkewOnto(buildErrorPayload(message))),
522
+ },
523
+ ],
518
524
  };
519
525
  }
520
526
  });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Reconnect backoff, shared by the SSE stream and the POST leg.
3
+ *
4
+ * Its own module only because both halves need it and they must not import each other: the proxy
5
+ * owns the stream, the post transport owns the request, and a cycle between them to reach three
6
+ * lines of arithmetic would be the wrong price.
7
+ */
8
+ /** Linear and capped, so a briefly-restarting daemon is picked up fast. */
9
+ export declare const RECONNECT_BASE_MS = 250;
10
+ export declare const RECONNECT_CAP_MS = 5000;
11
+ export declare function reconnectDelayMs(attempt: number): number;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Reconnect backoff, shared by the SSE stream and the POST leg.
3
+ *
4
+ * Its own module only because both halves need it and they must not import each other: the proxy
5
+ * owns the stream, the post transport owns the request, and a cycle between them to reach three
6
+ * lines of arithmetic would be the wrong price.
7
+ */
8
+ /** Linear and capped, so a briefly-restarting daemon is picked up fast. */
9
+ export const RECONNECT_BASE_MS = 250;
10
+ export const RECONNECT_CAP_MS = 5_000;
11
+ export function reconnectDelayMs(attempt) {
12
+ return Math.min(RECONNECT_BASE_MS * attempt, RECONNECT_CAP_MS);
13
+ }
@@ -1,3 +1,4 @@
1
+ export declare function daemonPollDelayMs(attempt: number): number;
1
2
  /**
2
3
  * Returns true if something is already listening on the reticle port.
3
4
  * Uses a plain TCP probe so we don't create a side-effectful SSE session
@@ -11,9 +11,13 @@ const DAEMON_READY_TIMEOUT_MS = Number.isFinite(envDaemonReadyTimeoutMs) && envD
11
11
  ? envDaemonReadyTimeoutMs
12
12
  : DEFAULT_DAEMON_READY_TIMEOUT_MS;
13
13
  const DAEMON_POLL_INTERVAL_MS = 100;
14
+ const DAEMON_POLL_MAX_INTERVAL_MS = 1_000;
14
15
  function delay(ms) {
15
16
  return new Promise((resolve) => setTimeout(resolve, ms));
16
17
  }
18
+ export function daemonPollDelayMs(attempt) {
19
+ return Math.min(DAEMON_POLL_INTERVAL_MS * attempt, DAEMON_POLL_MAX_INTERVAL_MS);
20
+ }
17
21
  /**
18
22
  * Returns true if something is already listening on the reticle port.
19
23
  * Uses a plain TCP probe so we don't create a side-effectful SSE session
@@ -38,11 +42,13 @@ export function probeDaemon(port) {
38
42
  /** Poll until the daemon's HTTP port accepts connections or the deadline is reached. */
39
43
  export async function waitForDaemon(port) {
40
44
  const deadline = Date.now() + DAEMON_READY_TIMEOUT_MS;
45
+ let attempt = 0;
41
46
  while (Date.now() < deadline) {
42
47
  const reachable = await probeDaemon(port);
43
48
  if (reachable)
44
49
  return;
45
- await delay(DAEMON_POLL_INTERVAL_MS);
50
+ attempt++;
51
+ await delay(daemonPollDelayMs(attempt));
46
52
  }
47
53
  throw new Error(`reticle daemon did not become ready on port ${port} within ${DAEMON_READY_TIMEOUT_MS}ms`);
48
54
  }
@@ -44,6 +44,29 @@ instructions?: string): string | null;
44
44
  * where it is honoured and costs one line where it is not.
45
45
  */
46
46
  export declare function toolsChangedNotification(catalogWasLocal: boolean): string | null;
47
+ /**
48
+ * What a locally-answered handshake must say about itself.
49
+ *
50
+ * The proxy answering `initialize` is right — a hang gives the agent no tools AND no diagnosis — but
51
+ * the response it sent was INDISTINGUISHABLE from a daemon's. Same `serverInfo`, same capabilities,
52
+ * same `instructions`. So the client marked the server installed and enabled, the agent got a tool
53
+ * list that was cached at best and empty at worst, and nothing anywhere connected the two facts.
54
+ *
55
+ * Reported from the field, from two different agent hosts at once: the marketplace showed Reticle
56
+ * installed with the whole tool surface enabled, and the agent asked to verify a flow reported that
57
+ * RETICLE WAS NOT PRESENT ON THE MACHINE — while the daemon was running and a browser session was
58
+ * live. That conclusion is the reasonable reading of an empty surface with no explanation attached,
59
+ * and it is unrecoverable: the proxy's own recovery is "the next client request re-probes the port",
60
+ * and a client holding zero tools never makes another request.
61
+ *
62
+ * `instructions` is the channel because it is the only one that survives an empty catalog. It rides
63
+ * on the initialize result, which SUCCEEDED, and every host we target puts it in front of the model.
64
+ * A tool description cannot reach an agent that was handed no tools.
65
+ *
66
+ * The healthy block is kept underneath rather than replaced: an agent that IS about to get a working
67
+ * surface (the common case — the first call wakes a daemon) still needs to know how to drive it.
68
+ */
69
+ export declare function degradedInstructions(healthy: string, port: number, reason: string): string;
47
70
  /**
48
71
  * The largest single JSON-RPC line the proxy will accumulate from a client.
49
72
  *
@@ -103,6 +103,37 @@ export function toolsChangedNotification(catalogWasLocal) {
103
103
  return null;
104
104
  return JSON.stringify({ jsonrpc: '2.0', method: TOOLS_CHANGED_NOTIFICATION });
105
105
  }
106
+ /**
107
+ * What a locally-answered handshake must say about itself.
108
+ *
109
+ * The proxy answering `initialize` is right — a hang gives the agent no tools AND no diagnosis — but
110
+ * the response it sent was INDISTINGUISHABLE from a daemon's. Same `serverInfo`, same capabilities,
111
+ * same `instructions`. So the client marked the server installed and enabled, the agent got a tool
112
+ * list that was cached at best and empty at worst, and nothing anywhere connected the two facts.
113
+ *
114
+ * Reported from the field, from two different agent hosts at once: the marketplace showed Reticle
115
+ * installed with the whole tool surface enabled, and the agent asked to verify a flow reported that
116
+ * RETICLE WAS NOT PRESENT ON THE MACHINE — while the daemon was running and a browser session was
117
+ * live. That conclusion is the reasonable reading of an empty surface with no explanation attached,
118
+ * and it is unrecoverable: the proxy's own recovery is "the next client request re-probes the port",
119
+ * and a client holding zero tools never makes another request.
120
+ *
121
+ * `instructions` is the channel because it is the only one that survives an empty catalog. It rides
122
+ * on the initialize result, which SUCCEEDED, and every host we target puts it in front of the model.
123
+ * A tool description cannot reach an agent that was handed no tools.
124
+ *
125
+ * The healthy block is kept underneath rather than replaced: an agent that IS about to get a working
126
+ * surface (the common case — the first call wakes a daemon) still needs to know how to drive it.
127
+ */
128
+ export function degradedInstructions(healthy, port, reason) {
129
+ return `DEGRADED SURFACE — READ THIS BEFORE ANY reticle_* CALL: no Reticle daemon answered on port ${String(port)} (${reason}), so this handshake was completed by the Reticle MCP proxy itself, not by Reticle's daemon. Any tool list you were handed came from a cache or is empty — it is not evidence that anything is behind it.
130
+
131
+ Reticle IS installed: this process is Reticle. Do NOT report it as missing, not present, or not installed — that is the wrong diagnosis and it is the one this state reliably produces. The right one is that the daemon is not reachable yet.
132
+
133
+ Your next reticle_* call starts a daemon and then either works or fails with the specific reason. If you have NO reticle_* tools at all, say exactly that to the user and tell them to run \`reticle doctor\` in a terminal — it reports what is holding port ${String(port)} and whether a daemon can start at all.
134
+
135
+ ${healthy}`;
136
+ }
106
137
  /**
107
138
  * The largest single JSON-RPC line the proxy will accumulate from a client.
108
139
  *
@@ -23,6 +23,14 @@
23
23
  * status`'s "registered, never enumerated" state — see attach-memory.ts.
24
24
  */
25
25
  export declare function isToolsListRequest(line: string): boolean;
26
+ /**
27
+ * Is this line a client CALLING a tool?
28
+ *
29
+ * The one fact that separates a working agent link from a merely-registered one. Listing the tools
30
+ * proves a client read the catalogue; it proves nothing about whether a request can cross to the
31
+ * daemon, and the field reports those two states looking identical from every other angle.
32
+ */
33
+ export declare function isToolCallRequest(line: string): boolean;
26
34
  export declare class ToolCatalogCache {
27
35
  #private;
28
36
  /** Watch a line coming BACK from the daemon; remember it if it is a tool catalog. */
@@ -35,6 +35,18 @@ function parse(line) {
35
35
  export function isToolsListRequest(line) {
36
36
  return parse(line)?.method === LIST_METHOD;
37
37
  }
38
+ /** The JSON-RPC method a client sends to actually USE a tool, as opposed to listing them. */
39
+ const CALL_METHOD = 'tools/call';
40
+ /**
41
+ * Is this line a client CALLING a tool?
42
+ *
43
+ * The one fact that separates a working agent link from a merely-registered one. Listing the tools
44
+ * proves a client read the catalogue; it proves nothing about whether a request can cross to the
45
+ * daemon, and the field reports those two states looking identical from every other angle.
46
+ */
47
+ export function isToolCallRequest(line) {
48
+ return parse(line)?.method === CALL_METHOD;
49
+ }
38
50
  export class ToolCatalogCache {
39
51
  #tools;
40
52
  /** Watch a line coming BACK from the daemon; remember it if it is a tool catalog. */
@@ -0,0 +1,2 @@
1
+ import type { ToolDef } from '../tools/tools.js';
2
+ export declare const MEMORY_TOOLS: ToolDef[];
@@ -0,0 +1,82 @@
1
+ /**
2
+ * `reticle_memory` — ask the project what it already knows.
3
+ *
4
+ * Shared memory reached three callers before this one: the sync writes it, flow replay consults it
5
+ * automatically, and `reticle memory` prints it at a terminal. The one who could not ask was the
6
+ * agent doing the work — mid-drive, holding a question, with a whole tool surface and nothing on it
7
+ * for "what has this team already established about checkout?".
8
+ *
9
+ * That matters twice over. An agent that can read the corpus stops re-deriving what a colleague
10
+ * proved last week; and because the read is attributed, the coverage map's fetch counts finally
11
+ * measure something — they were zero across an entire real corpus, not because the knowledge was
12
+ * useless but because consulting it was a separate act nobody performed.
13
+ */
14
+ import { z } from 'zod';
15
+ import { ReticleTool } from '../tools/tool-names.js';
16
+ import { countSchema } from '../tools/numeric-bounds.js';
17
+ import { sessionIdShape } from '../tools/tool-kit.js';
18
+ import { asNumber, asString } from '../tools/tools-helpers.js';
19
+ import { sessionRoot } from '../project/session-root.js';
20
+ import { MemoryUnavailable, readProjectMemory } from './project-memory.js';
21
+ import { homedir } from 'node:os';
22
+ /**
23
+ * Enough to orient on, not enough to flood a context.
24
+ *
25
+ * A busy subject on a real project runs to dozens of statements; an agent asking a question wants
26
+ * the ones that settle it. `total` always rides along, so the cap is never silent.
27
+ */
28
+ const DEFAULT_LIMIT = 10;
29
+ /** What to do next, per reason. A refusal that does not say how to fix it is a dead end. */
30
+ const ADVICE = {
31
+ [MemoryUnavailable.NOT_LINKED]: 'this project is not linked to a Reticle workspace, so there is no shared memory to read. Run `npx @reticlehq/server login` in the project to link it.',
32
+ [MemoryUnavailable.DISABLED]: 'this project is linked but memory sync is turned off in .reticle/cloud.json, so nothing is being shared. Enable `sync.memory` to use it.',
33
+ [MemoryUnavailable.UNREACHABLE]: 'the workspace could not be reached, or refused the key. Nothing is wrong with this project — try again, or check the link with `reticle status`.',
34
+ };
35
+ export const MEMORY_TOOLS = [
36
+ {
37
+ name: ReticleTool.MEMORY,
38
+ description: 'Ask what THIS PROJECT already knows — the shared memory its team has built up, written by every engineer who verified something and readable by every agent since. Use it BEFORE re-deriving how a feature is supposed to behave: a colleague may already have proved it, and a statement marked `proved` was established by a real verdict rather than written down. With { subject } it narrows to one area (a flow name, or a route like "checkout"); without, it returns across the project. Each entry carries the flow and the source file it is about, so a claim can be checked. The read is recorded as an agent consultation, which is what the dashboard\'s per-flow fetch counts are built from. Returns { subject, known, total } or { error, reason, advice } when this project is not linked to a workspace.',
39
+ inputSchema: {
40
+ subject: z
41
+ .string()
42
+ .optional()
43
+ .describe('Narrow to one area — a flow name ("checkout-pay") or a route segment ("issues"). Omit to read across the whole project. Subjects come from the coverage map; an unknown one returns nothing rather than erroring.'),
44
+ limit: countSchema
45
+ .optional()
46
+ .describe(`Most relevant N statements. Defaults to ${String(DEFAULT_LIMIT)}. Proved statements rank first, so a small limit still returns the settled ones. \`total\` always reports the true count.`),
47
+ ...sessionIdShape,
48
+ },
49
+ outputSchema: {
50
+ subject: z.string().nullable().optional(),
51
+ known: z.array(z.unknown()).optional(),
52
+ total: z.number().optional(),
53
+ error: z.string().optional(),
54
+ reason: z.string().optional(),
55
+ advice: z.string().optional(),
56
+ },
57
+ handler: async (deps, args) => {
58
+ /*
59
+ * The APP's root, not the daemon's.
60
+ *
61
+ * `deps.reticleRoot` is wherever the daemon was launched, which for a user-scoped MCP
62
+ * registration is almost never the project being driven — so the link file it reads is the
63
+ * wrong one or absent, and every project silently reads as "not attached". The same defect
64
+ * has now been fixed three times in this codebase; routing through sessionRoot is what stops
65
+ * a fourth.
66
+ */
67
+ const root = sessionRoot(deps, asString(args['sessionId']));
68
+ const subject = asString(args['subject']);
69
+ const result = await readProjectMemory(deps.fs, root, homedir(), process.env, {
70
+ subject,
71
+ limit: asNumber(args['limit']) ?? DEFAULT_LIMIT,
72
+ });
73
+ if (!result.ok)
74
+ return {
75
+ error: 'no shared memory available for this project',
76
+ reason: result.reason,
77
+ advice: ADVICE[result.reason],
78
+ };
79
+ return { subject: result.subject, known: result.known, total: result.total };
80
+ },
81
+ },
82
+ ];
@@ -0,0 +1,34 @@
1
+ import type { FileSystemPort } from '../project/fs-port.js';
2
+ /** Named so a read from the tool surface is distinguishable from the CLI's and from replay's. */
3
+ export declare const MCP_AGENT_ID = "reticle-mcp";
4
+ /** Why a lookup returned nothing. Each one is a different thing for the agent to do next. */
5
+ export declare const MemoryUnavailable: {
6
+ /** No `.reticle/cloud.json` — this project has never been linked to a workspace. */
7
+ readonly NOT_LINKED: "not-linked";
8
+ /** Linked, but the link says not to sync memory. A setting, not a failure. */
9
+ readonly DISABLED: "memory-sync-disabled";
10
+ /** The server was reached and said no, or could not be reached at all. */
11
+ readonly UNREACHABLE: "unreachable";
12
+ };
13
+ export type MemoryUnavailable = (typeof MemoryUnavailable)[keyof typeof MemoryUnavailable];
14
+ export interface KnownThing {
15
+ statement: string;
16
+ status: string;
17
+ flowName: string | null;
18
+ sourceFile: string | null;
19
+ subject: string;
20
+ }
21
+ export type ProjectMemoryResult = {
22
+ ok: true;
23
+ subject: string | null;
24
+ known: KnownThing[];
25
+ total: number;
26
+ } | {
27
+ ok: false;
28
+ reason: MemoryUnavailable;
29
+ };
30
+ export declare const rankKnown: (things: readonly KnownThing[]) => KnownThing[];
31
+ export declare function readProjectMemory(fs: FileSystemPort, root: string, home: string, env: NodeJS.ProcessEnv, opts: {
32
+ subject?: string | undefined;
33
+ limit: number;
34
+ }): Promise<ProjectMemoryResult>;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * What the PROJECT knows, read on demand.
3
+ *
4
+ * Shared memory reached three places before this one: the sync writes it, flow replay consults it
5
+ * automatically, and `reticle memory` prints it at a terminal. The one caller who could not ask was
6
+ * the agent actually doing the work — mid-drive, holding a question, with 44 tools and none of them
7
+ * "what does this team already know about checkout?". That is the gap this closes, and it is the
8
+ * difference between a corpus and a wiki nobody opens.
9
+ *
10
+ * The read is ATTRIBUTED. It carries an agent header, so the coverage map counts it as a
11
+ * consultation and a manager can see which flows are actually being pulled and by whom — the
12
+ * question the fetch counters exist to answer and could not while nothing was reading.
13
+ */
14
+ import { cloudFetch } from '../cloud/cloud-sync.js';
15
+ import { resolveProjectCloud } from '../cloud/cloud-config.js';
16
+ /** Named so a read from the tool surface is distinguishable from the CLI's and from replay's. */
17
+ export const MCP_AGENT_ID = 'reticle-mcp';
18
+ /** Why a lookup returned nothing. Each one is a different thing for the agent to do next. */
19
+ export const MemoryUnavailable = {
20
+ /** No `.reticle/cloud.json` — this project has never been linked to a workspace. */
21
+ NOT_LINKED: 'not-linked',
22
+ /** Linked, but the link says not to sync memory. A setting, not a failure. */
23
+ DISABLED: 'memory-sync-disabled',
24
+ /** The server was reached and said no, or could not be reached at all. */
25
+ UNREACHABLE: 'unreachable',
26
+ };
27
+ /** Defensive: the wire is somebody else's server, so every field is checked before it is trusted. */
28
+ const asKnown = (raw) => {
29
+ if (null === raw || typeof raw !== 'object')
30
+ return null;
31
+ const r = raw;
32
+ const statement = r['statement'];
33
+ if (typeof statement !== 'string' || 0 === statement.length)
34
+ return null;
35
+ return {
36
+ statement,
37
+ status: 'string' === typeof r['status'] ? r['status'] : 'unknown',
38
+ flowName: 'string' === typeof r['flowName'] ? r['flowName'] : null,
39
+ sourceFile: 'string' === typeof r['sourceFile'] ? r['sourceFile'] : null,
40
+ subject: 'string' === typeof r['subject'] ? r['subject'] : 'unsorted',
41
+ };
42
+ };
43
+ /**
44
+ * Proved first, then the rest.
45
+ *
46
+ * An agent reading this is about to act on it, and a statement a verdict has actually established
47
+ * is worth more than one somebody merely wrote down. Stable within each group so the same call
48
+ * twice returns the same order — an unstable list reads as the corpus churning when it has not.
49
+ */
50
+ const PROVED = 'proved';
51
+ export const rankKnown = (things) => [
52
+ ...things.filter((t) => PROVED === t.status),
53
+ ...things.filter((t) => PROVED !== t.status),
54
+ ];
55
+ export async function readProjectMemory(fs, root, home, env, opts) {
56
+ const cloud = await resolveProjectCloud(fs, root, home, env);
57
+ if (null === cloud.config)
58
+ return { ok: false, reason: MemoryUnavailable.NOT_LINKED };
59
+ if (!cloud.policy.memory)
60
+ return { ok: false, reason: MemoryUnavailable.DISABLED };
61
+ const query = opts.subject === undefined ? '' : `?subject=${encodeURIComponent(opts.subject)}`;
62
+ try {
63
+ const res = await cloudFetch(`${cloud.config.url}/v1/memory${query}`, {
64
+ method: 'GET',
65
+ headers: {
66
+ authorization: `Bearer ${cloud.config.apiKey}`,
67
+ // What makes this read COUNT as an agent consulting the corpus rather than a human
68
+ // browsing — the distinction the dashboard's "times an agent consulted it" rests on.
69
+ 'x-reticle-agent': MCP_AGENT_ID,
70
+ },
71
+ });
72
+ if (200 !== res.status)
73
+ return { ok: false, reason: MemoryUnavailable.UNREACHABLE };
74
+ // `cloudFetch` returns a real Response, so the body is a METHOD. Reading `res.json` as a
75
+ // property yields the function, and every downstream field is undefined — a silent "the project
76
+ // knows nothing" that is indistinguishable from the honest empty case. It cost a live drive to
77
+ // find once already.
78
+ const body = (await res.json());
79
+ const entries = body?.entries;
80
+ if (!Array.isArray(entries))
81
+ return { ok: false, reason: MemoryUnavailable.UNREACHABLE };
82
+ const known = entries.map(asKnown).filter((k) => k !== null);
83
+ return {
84
+ ok: true,
85
+ subject: opts.subject ?? null,
86
+ // Capped for the agent's context, but `total` always reports the truth so the cap is never
87
+ // silent — a truncated list that looks complete is how an agent concludes a project knows
88
+ // less than it does.
89
+ known: rankKnown(known).slice(0, opts.limit),
90
+ total: known.length,
91
+ };
92
+ }
93
+ catch {
94
+ return { ok: false, reason: MemoryUnavailable.UNREACHABLE };
95
+ }
96
+ }
@@ -1,4 +1,5 @@
1
1
  import { EventType, PredicateKind } from '@reticlehq/core';
2
+ import { routePathOf } from '../events/predicate-route.js';
2
3
  const TIER = { SIGNAL: 0, CONSEQUENCE: 1, PRESENCE: 2 };
3
4
  function pathname(url) {
4
5
  if (typeof url !== 'string')
@@ -66,10 +67,18 @@ export function proposeConsequences(events) {
66
67
  break;
67
68
  }
68
69
  case EventType.ROUTE_CHANGE: {
69
- const to = data['pathname'];
70
- if ('string' === typeof to) {
70
+ const docPath = data['pathname'];
71
+ if ('string' === typeof docPath) {
72
+ // `pathname`, not `to`: the evaluator has never accepted `to`, so this surface was handing
73
+ // the agent a predicate reticle_assert refuses with "unknown field to". A proposal that
74
+ // cannot be evaluated is worse than no proposal — see the schema check in the test.
75
+ //
76
+ // And the ROUTER's path, not the document's: under a hash router the document pathname is
77
+ // `/` on every page, so proposing it proposes an assertion that says nothing about where
78
+ // the app went.
79
+ const to = routePathOf(docPath, 'string' === typeof data['hash'] ? data['hash'] : '');
71
80
  add(`route:${to}`, {
72
- predicate: { kind: PredicateKind.ROUTE, to },
81
+ predicate: { kind: PredicateKind.ROUTE, pathname: to },
73
82
  tier: TIER.CONSEQUENCE,
74
83
  label: `route changes to ${to}`,
75
84
  weak: false,