@reticlehq/server 2.12.0 → 2.13.1

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 +68 -0
  253. package/dist/setup/dev-server-wait.js +67 -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 +203 -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
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The contract between the agent and the script.
3
+ *
4
+ * Everything setup does is deterministic except for the handful of things that require reading a
5
+ * repository and a human's request and understanding both. Those are exactly what an agent is good
6
+ * at and code is not, so they arrive here as ARGUMENTS rather than as steps somebody walks through:
7
+ * the agent decides, the script executes, and neither does the other's job.
8
+ *
9
+ * What only an agent can know, and therefore what belongs in this type:
10
+ *
11
+ * --flow which journey proves the thing the user cares about. Code can enumerate buttons; it
12
+ * cannot know that checkout matters and the theme toggle does not. Measured, naming
13
+ * it took one app's drive from a ten-minute timeout to 138 seconds, because the turns
14
+ * go into FINDING a flow and naming one removes that search.
15
+ * --app which app in a monorepo the user meant. init can find the ones that are servable;
16
+ * only the request says which of them is being worked on.
17
+ * --env what the app needs to get past its own front door. A key in .env.example, a mock
18
+ * backend named in the README, the variable that skips an auth wall: all readable by
19
+ * an agent, none inferable by a script. Without this a real app sits on a login
20
+ * screen and there is no flow to drive at all.
21
+ * --dev-cmd the command, when the project's own scripts do not say it plainly.
22
+ * --url the app is already running here, so do not start anything.
23
+ *
24
+ * Everything else on this type is an operator's dial, not an agent's judgement.
25
+ */
26
+ /** The bridge port. NOT the dev server's, and conflating them is a documented setup failure. */
27
+ export const DEFAULT_BRIDGE_PORT = 4400;
28
+ /** Per-phase budget. The drive gets its own, much larger one. */
29
+ export const DEFAULT_PHASE_TIMEOUT_MS = 120_000;
30
+ /** What the drive may spend before it is stopped. */
31
+ export const DEFAULT_DRIVE_BUDGET_USD = 3;
32
+ export const DEFAULT_SETUP_OPTIONS = {
33
+ bridgePort: DEFAULT_BRIDGE_PORT,
34
+ phaseTimeoutMs: DEFAULT_PHASE_TIMEOUT_MS,
35
+ driveBudgetUsd: DEFAULT_DRIVE_BUDGET_USD,
36
+ registerAgents: true,
37
+ openBrowser: true,
38
+ drive: true,
39
+ escalateWeakFlow: true,
40
+ restartStaleDevServer: true,
41
+ filesOnly: false,
42
+ };
43
+ /** One `KEY=VALUE`, as an agent would pass it. Everything after the first `=` is the value. */
44
+ export function parseEnvAssignment(pair) {
45
+ const at = pair.indexOf('=');
46
+ if (0 >= at)
47
+ return null;
48
+ const key = pair.slice(0, at).trim();
49
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
50
+ return null;
51
+ return { key, value: pair.slice(at + 1) };
52
+ }
53
+ /**
54
+ * Fold repeated `--env KEY=VALUE` into one map, ignoring what cannot be a variable.
55
+ *
56
+ * A malformed pair is dropped rather than guessed at: an agent that meant `--env` and typed
57
+ * something else should see its variable missing, not see setup invent one.
58
+ */
59
+ export function collectEnv(pairs) {
60
+ const env = {};
61
+ for (const pair of pairs) {
62
+ const parsed = parseEnvAssignment(pair);
63
+ if (null !== parsed)
64
+ env[parsed.key] = parsed.value;
65
+ }
66
+ return env;
67
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Does a transcript exist for this Claude Code session.
3
+ *
4
+ * Two roots, because a shared-config install writes elsewhere and a restart that checks only one of
5
+ * them refuses a session that is perfectly real.
6
+ */
7
+ export declare function claudeTranscriptExists(sessionId: string): boolean;
8
+ /**
9
+ * The codex conversation running in `cwd`, when one is.
10
+ *
11
+ * Codex names no session in the environment — unlike Claude Code — so it is recognised from the
12
+ * rollout file it is writing. Not yet wired to a reader here: returning undefined means "no codex
13
+ * session identified", which is the honest answer until one exists, and the decision it feeds says
14
+ * plainly that the client identified no conversation.
15
+ */
16
+ export declare function codexSessionFor(_cwd: string): string | undefined;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Which conversation is asking, and does it really exist.
3
+ *
4
+ * Both answers come off the filesystem, so they are kept away from the pure decision in relaunch.ts
5
+ * — that is the part worth testing, and it is the part that can be wrong without anybody noticing.
6
+ */
7
+ import { existsSync, readdirSync } from 'node:fs';
8
+ import { homedir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ /**
11
+ * Does a transcript exist for this Claude Code session.
12
+ *
13
+ * Two roots, because a shared-config install writes elsewhere and a restart that checks only one of
14
+ * them refuses a session that is perfectly real.
15
+ */
16
+ export function claudeTranscriptExists(sessionId) {
17
+ for (const base of [
18
+ join(homedir(), '.claude-shared', 'projects'),
19
+ join(homedir(), '.claude', 'projects'),
20
+ ]) {
21
+ try {
22
+ for (const project of readdirSync(base)) {
23
+ if (existsSync(join(base, project, `${sessionId}.jsonl`)))
24
+ return true;
25
+ }
26
+ }
27
+ catch {
28
+ /* that history directory does not exist on this machine */
29
+ }
30
+ }
31
+ return false;
32
+ }
33
+ /**
34
+ * The codex conversation running in `cwd`, when one is.
35
+ *
36
+ * Codex names no session in the environment — unlike Claude Code — so it is recognised from the
37
+ * rollout file it is writing. Not yet wired to a reader here: returning undefined means "no codex
38
+ * session identified", which is the honest answer until one exists, and the decision it feeds says
39
+ * plainly that the client identified no conversation.
40
+ */
41
+ export function codexSessionFor(_cwd) {
42
+ return undefined;
43
+ }
@@ -24,8 +24,21 @@ interface StatusPayload {
24
24
  * running the command we tell them to run in `init`'s closing line deserves the same sentence.
25
25
  */
26
26
  why?: string;
27
+ /** Port of the verify HTTP endpoint this daemon serves — present only when started with `--http`. */
28
+ verifyPort?: number;
27
29
  }
28
30
  export declare function statusPayload(sessionCount: number, sessions: SessionInfo[],
29
31
  /** The no-session diagnosis, injected so this stays pure and the port probe stays off this path. */
30
- why?: string): StatusPayload;
32
+ why?: string, verifyPort?: number): StatusPayload;
33
+ /**
34
+ * Why `serve --http` cannot claim success against an already-running daemon — or undefined when
35
+ * that daemon already serves the verify HTTP endpoint on the wanted port.
36
+ *
37
+ * `serve` never hands flags to a daemon that is already up, so `--http`/`--http-port` against one
38
+ * used to be accepted, dropped, and reported as success (#687). This is the read side of the
39
+ * `verifyPort` field above: the daemon says which port it serves, and `serve` compares. A daemon
40
+ * too old to report the field — or one that did not answer /status — reads as "not honoured",
41
+ * which errs loud rather than green.
42
+ */
43
+ export declare function verifyEndpointMismatch(status: unknown, wantedPort: number): string | undefined;
31
44
  export {};
@@ -2,7 +2,7 @@ import { CONTRACT_FINGERPRINT } from '@reticlehq/core';
2
2
  import { SERVER_VERSION } from './version/server-version.js';
3
3
  export function statusPayload(sessionCount, sessions,
4
4
  /** The no-session diagnosis, injected so this stays pure and the port probe stays off this path. */
5
- why) {
5
+ why, verifyPort) {
6
6
  return {
7
7
  running: true,
8
8
  version: SERVER_VERSION,
@@ -12,5 +12,27 @@ why) {
12
12
  // Only when there is nothing to explain away: a diagnosis printed beside a live session would
13
13
  // contradict it.
14
14
  ...(0 === sessionCount && why !== undefined ? { why } : {}),
15
+ ...(verifyPort === undefined ? {} : { verifyPort }),
15
16
  };
16
17
  }
18
+ /**
19
+ * Why `serve --http` cannot claim success against an already-running daemon — or undefined when
20
+ * that daemon already serves the verify HTTP endpoint on the wanted port.
21
+ *
22
+ * `serve` never hands flags to a daemon that is already up, so `--http`/`--http-port` against one
23
+ * used to be accepted, dropped, and reported as success (#687). This is the read side of the
24
+ * `verifyPort` field above: the daemon says which port it serves, and `serve` compares. A daemon
25
+ * too old to report the field — or one that did not answer /status — reads as "not honoured",
26
+ * which errs loud rather than green.
27
+ */
28
+ export function verifyEndpointMismatch(status, wantedPort) {
29
+ const served = 'object' === typeof status && status !== null
30
+ ? status['verifyPort']
31
+ : undefined;
32
+ if (served === wantedPort)
33
+ return undefined;
34
+ const fix = 'stop it (`reticle stop`) and run `reticle serve --http` again';
35
+ return 'number' === typeof served
36
+ ? `the daemon already running serves the verify HTTP endpoint on :${String(served)}, not :${String(wantedPort)} — ${fix}`
37
+ : `a daemon is already running without the verify HTTP endpoint — \`--http\` cannot be applied to it; ${fix}`;
38
+ }
@@ -53,4 +53,11 @@ export interface DaemonTelemetry {
53
53
  * Report the daemon as started, profile the project once, and keep the session counters flushing.
54
54
  * Every timer is `unref`'d so telemetry can never be the reason a daemon refuses to exit.
55
55
  */
56
- export declare function installDaemonTelemetry(cwd: string, now?: () => number): DaemonTelemetry;
56
+ export declare function installDaemonTelemetry(cwd: string, now?: () => number,
57
+ /**
58
+ * The bridge port this daemon bound, so the profile can say whether an app for this project has
59
+ * EVER connected. Optional: a caller that does not know the port gets a profile without that bit
60
+ * rather than one with a guessed `false`, which is the value the whole field exists to make
61
+ * trustworthy.
62
+ */
63
+ port?: number): DaemonTelemetry;
@@ -10,10 +10,34 @@ import { TelemetryEventKind } from '@reticlehq/core';
10
10
  import { getTelemetry } from './telemetry.js';
11
11
  import { getSessionMetrics } from './session-metrics.js';
12
12
  import { profileProject } from './project-profile.js';
13
- import { startUpdateCheck } from '../update/update-nudge.js';
13
+ import { hasProjectConnectedBefore } from '../session/connection-memory.js';
14
+ import { readProjectId } from '../cli/cli-port.js';
15
+ import { reticleStateHome } from '../daemon/daemon.js';
16
+ import { startUpdateCheck, updateNudgeState } from '../update/update-nudge.js';
14
17
  import { markDaemonStart } from './mcp-connection.js';
15
18
  import { markInstrumentationClock } from './app-instrumented.js';
16
19
  import { markStallClock, STALL_AFTER_MS, stallUptime } from '../session/stall-clock.js';
20
+ /**
21
+ * Whether an app for this project has ever reached Reticle, read from the same durable memory the
22
+ * no-session diagnosis and the server instructions read.
23
+ *
24
+ * Best-effort: an unreadable state home must never be the reason a daemon start emits nothing, and
25
+ * `undefined` here means "not measured" rather than "no". A `false` invented from a read error is
26
+ * exactly the lie this field exists to avoid — it would put the working installs into the cohort we
27
+ * are trying to size.
28
+ */
29
+ function installFacts(cwd, port) {
30
+ if (port === undefined)
31
+ return undefined;
32
+ try {
33
+ return {
34
+ appConnectedBefore: hasProjectConnectedBefore(reticleStateHome(), port, readProjectId(cwd)),
35
+ };
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ }
17
41
  /** Let the daemon finish coming up before walking the source tree for a profile. */
18
42
  const PROJECT_PROFILE_DELAY_MS = 5_000;
19
43
  /**
@@ -44,11 +68,34 @@ export const SESSION_FLUSH_MS = 5 * 60 * 1000;
44
68
  * and 74% of daemons never serve a tool at all.
45
69
  */
46
70
  export const FIRST_FLUSH_MS = 90 * 1000;
71
+ /**
72
+ * Attach what the update nudge did to a summary that is about to be sent.
73
+ *
74
+ * Merged HERE rather than inside `SessionMetrics` deliberately: the nudge's state lives in the
75
+ * update module, and a second copy of it in the metrics object would be one more thing that can
76
+ * drift from the flag the delivery path actually reads. `summarize()` stays a pure roll-up of
77
+ * counters.
78
+ */
79
+ function withNudgeState(summary) {
80
+ const nudge = updateNudgeState();
81
+ return {
82
+ ...summary,
83
+ updateNudged: nudge.shown,
84
+ ...(nudge.offered === undefined ? {} : { updateOffered: nudge.offered }),
85
+ };
86
+ }
47
87
  /**
48
88
  * Report the daemon as started, profile the project once, and keep the session counters flushing.
49
89
  * Every timer is `unref`'d so telemetry can never be the reason a daemon refuses to exit.
50
90
  */
51
- export function installDaemonTelemetry(cwd, now = () => Date.now()) {
91
+ export function installDaemonTelemetry(cwd, now = () => Date.now(),
92
+ /**
93
+ * The bridge port this daemon bound, so the profile can say whether an app for this project has
94
+ * EVER connected. Optional: a caller that does not know the port gets a profile without that bit
95
+ * rather than one with a guessed `false`, which is the value the whole field exists to make
96
+ * trustworthy.
97
+ */
98
+ port) {
52
99
  const metrics = getSessionMetrics(now);
53
100
  void getTelemetry().emit(TelemetryEventKind.DAEMON_STARTED);
54
101
  // Ask npm whether a newer Reticle exists, so the agent can tell the human. Nothing checked before
@@ -66,7 +113,7 @@ export function installDaemonTelemetry(cwd, now = () => Date.now()) {
66
113
  setTimeout(() => {
67
114
  try {
68
115
  void getTelemetry().emit(TelemetryEventKind.PROJECT_PROFILED, {
69
- project: profileProject(cwd, now()),
116
+ project: profileProject(cwd, now(), installFacts(cwd, port)),
70
117
  });
71
118
  }
72
119
  catch {
@@ -93,7 +140,7 @@ export function installDaemonTelemetry(cwd, now = () => Date.now()) {
93
140
  return; // an idle window is not worth an event
94
141
  // NOT daemon_stopped: the daemon is still running. See SESSION_PROGRESS.
95
142
  void getTelemetry().emit(TelemetryEventKind.SESSION_PROGRESS, {
96
- session: metrics.summarize(false),
143
+ session: withNudgeState(metrics.summarize(false)),
97
144
  });
98
145
  metrics.reset();
99
146
  };
@@ -114,7 +161,7 @@ export function installDaemonTelemetry(cwd, now = () => Date.now()) {
114
161
  // The rich one: the whole session in a single event — duration, the tool histogram, error
115
162
  // shapes, verifications, browser launches. This is what replaced the per-tool-call event.
116
163
  await getTelemetry().emit(TelemetryEventKind.DAEMON_STOPPED, {
117
- session: metrics.summarize(true, exit),
164
+ session: withNudgeState(metrics.summarize(true, exit)),
118
165
  });
119
166
  })();
120
167
  return stopped;
@@ -30,7 +30,7 @@ export const ErrorClass = {
30
30
  const PATTERNS = [
31
31
  [/confirmdangerous|destructive action blocked/i, ErrorClass.REFUSAL],
32
32
  [
33
- /\brefus(e|ed|es|ing)\b|not supported|unsupported|cannot .* a <|contenteditable/i,
33
+ /\brefus(e|ed|es|ing)\b|not supported|unsupported|cannot .* a <|contenteditable|cannot hover/i,
34
34
  ErrorClass.REFUSAL,
35
35
  ],
36
36
  [
@@ -1,10 +1,11 @@
1
- import { McpScope, type Feedback } from '@reticlehq/core';
1
+ import { McpScope, StackUnknownReason, type Feedback } from '@reticlehq/core';
2
2
  /** The context fields — the whole `Feedback` shape minus what the author supplies. */
3
3
  export type FeedbackContext = Pick<Feedback, 'stack' | 'stackMajor' | 'runtime' | 'engine' | 'driver' | 'client' | 'clientVersion' | 'mcpScope'>;
4
4
  export declare function detectStack(cwd: string, read?: (path: string) => string): {
5
5
  stack?: string;
6
6
  stackMajor?: number;
7
7
  stackSource?: 'cwd' | 'workspace';
8
+ stackUnknownReason?: StackUnknownReason;
8
9
  };
9
10
  /**
10
11
  * User scope unless the project checks in its own MCP registration. `reticle init` deliberately
@@ -12,6 +13,13 @@ export declare function detectStack(cwd: string, read?: (path: string) => string
12
13
  * itself the interesting signal.
13
14
  */
14
15
  export declare function detectMcpScope(cwd: string, exists?: (path: string) => boolean): McpScope;
16
+ /**
17
+ * The MCP client on the other end, self-reported in its `initialize` handshake (`claude-code`,
18
+ * `cursor-vscode`, …). Read through a HOOK rather than sniffing env vars: the handshake is the
19
+ * client's own claim about itself, while an env-var table is a guess that silently rots every time a
20
+ * vendor renames a variable. Unset (a CLI run with no MCP peer) simply reports no client.
21
+ */
22
+ /** Kept as the telemetry-facing name; the mechanism itself lives in mcp/client-identity.ts. */
15
23
  export declare function setMcpClientNameHook(hook: () => {
16
24
  name?: string;
17
25
  version?: string;
@@ -9,11 +9,12 @@
9
9
  * holds to. Detection is entirely best-effort: an unreadable package.json or an SDK too old to report
10
10
  * its runtime yields `undefined`, never a throw and never a guess.
11
11
  */
12
+ import { mcpClientIdentity, setMcpClientIdentityHook } from '../mcp/client-identity.js';
12
13
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
13
14
  import { isAbsolute, join } from 'node:path';
14
- import { AppRuntime, McpScope, PageDriver } from '@reticlehq/core';
15
+ import { AppRuntime, McpScope, PageDriver, StackUnknownReason, } from '@reticlehq/core';
15
16
  import { parseMajor } from '../init/detect.js';
16
- import { findWorkspaceApps } from '../init/run.js';
17
+ import { findWorkspaceApps } from '../init/workspace-apps.js';
17
18
  /**
18
19
  * Dependency → reported stack, in resolution order. Meta-frameworks come FIRST: a Next app also
19
20
  * depends on react, and reporting `react` for it would collapse the single most important segment
@@ -40,10 +41,14 @@ const PACKAGE_JSON = 'package.json';
40
41
  /** A project-scoped MCP registration lives in one of these; `reticle init` writes user scope instead. */
41
42
  const PROJECT_MCP_FILES = ['.mcp.json', join('.cursor', 'mcp.json'), join('.vscode', 'mcp.json')];
42
43
  /**
43
- * The project's framework and its MAJOR version, or `{}` when package.json is absent, unreadable, or
44
- * names nothing we recognize. The major comes from `parseMajor` — the same range parser `reticle init`
45
- * uses to decide React 19 source mapping — so a range like `^15.0.0-canary.3` narrows the same way in
46
- * both places instead of via a second, subtly different reader.
44
+ * The project's framework and its MAJOR version. The major comes from `parseMajor` — the same range
45
+ * parser `reticle init` uses to decide React 19 source mapping — so a range like `^15.0.0-canary.3`
46
+ * narrows the same way in both places instead of via a second, subtly different reader.
47
+ *
48
+ * `no-manifest` covers absent AND unreadable: both mean this directory could not answer, which is
49
+ * the distinction that matters to the caller. Unreadable is not separated because a manifest we
50
+ * cannot parse is not evidence about the app, and a reason nobody can act on is noise in a closed
51
+ * vocabulary.
47
52
  */
48
53
  function stackOfManifest(dir, read) {
49
54
  let pkg;
@@ -51,7 +56,7 @@ function stackOfManifest(dir, read) {
51
56
  pkg = JSON.parse(read(join(dir, PACKAGE_JSON)));
52
57
  }
53
58
  catch {
54
- return {};
59
+ return { kind: 'no-manifest' };
55
60
  }
56
61
  const deps = { ...pkg.devDependencies, ...pkg.dependencies };
57
62
  for (const [dep, stack] of STACK_BY_DEP) {
@@ -59,15 +64,17 @@ function stackOfManifest(dir, read) {
59
64
  if (range === undefined)
60
65
  continue;
61
66
  const stackMajor = parseMajor(range);
62
- return { stack, ...(stackMajor !== undefined ? { stackMajor } : {}) };
67
+ return { kind: 'found', stack, ...(stackMajor !== undefined ? { stackMajor } : {}) };
63
68
  }
64
- return {};
69
+ return { kind: 'unrecognised', declaresWorkspaces: pkg.workspaces !== undefined };
65
70
  }
66
71
  export function detectStack(cwd, read = readFile) {
67
72
  // The cwd wins when it IS an app — never redirect away from the real answer.
68
73
  const here = stackOfManifest(cwd, read);
69
- if (here.stack !== undefined)
70
- return { ...here, stackSource: 'cwd' };
74
+ if ('found' === here.kind) {
75
+ const { stack, stackMajor } = here;
76
+ return { stack, ...(stackMajor !== undefined ? { stackMajor } : {}), stackSource: 'cwd' };
77
+ }
71
78
  // Otherwise look for the app. The daemon's cwd is wherever the agent's client happened to launch,
72
79
  // which for a real repo is the ROOT while the app sits in `frontend/`, `web/`, or a declared
73
80
  // workspace. Reading one directory and giving up is why this returned nothing for every project
@@ -78,19 +85,53 @@ export function detectStack(cwd, read = readFile) {
78
85
  // declared workspaces (pnpm-workspace.yaml, package.json `workspaces`) AND scans top-level
79
86
  // directories, and it was widened once already for a repo shape a user hit twice. A second
80
87
  // detector here would drift from it the first time either changed.
88
+ //
89
+ // Every exit below carries WHY, because an empty result is not a cause. Four different facts used
90
+ // to arrive as one `{}`: no app anywhere, an app we read and did not recognise, workspace apps
91
+ // that were all unrecognised, and discovery erroring. They have different fixes, and the bucket
92
+ // was unusable while they were indistinguishable (#617).
93
+ let sawWorkspaceApp = false;
81
94
  try {
82
95
  for (const app of findWorkspaceApps(profileIo(cwd))) {
83
96
  const found = stackOfManifest(join(cwd, app), read);
84
97
  // Reported as `workspace` so the share of projects needing discovery is measurable — that
85
98
  // number is what says whether our inference needs an agent to correct it.
86
- if (found.stack !== undefined)
87
- return { ...found, stackSource: 'workspace' };
99
+ if ('found' === found.kind) {
100
+ const { stack, stackMajor } = found;
101
+ return {
102
+ stack,
103
+ ...(stackMajor !== undefined ? { stackMajor } : {}),
104
+ stackSource: 'workspace',
105
+ };
106
+ }
107
+ // A manifest we READ and did not recognise is evidence about the app; a directory with no
108
+ // manifest is not an app at all and says nothing either way.
109
+ if ('unrecognised' === found.kind)
110
+ sawWorkspaceApp = true;
88
111
  }
89
112
  }
90
113
  catch {
91
114
  // Discovery touches the filesystem; a permission error must never cost us the profile event.
115
+ // Named rather than folded into "found nothing": an error is not an absence, and a detector
116
+ // that reports one as the other sends the reader looking for a project that was there.
117
+ return { stackUnknownReason: StackUnknownReason.DISCOVERY_FAILED };
118
+ }
119
+ if (sawWorkspaceApp) {
120
+ return { stackUnknownReason: StackUnknownReason.WORKSPACE_APPS_UNRECOGNISED };
92
121
  }
93
- return {};
122
+ // The cwd's own manifest is the stronger statement when it exists: we read this app and did not
123
+ // know it. Ranked below the workspace answer only because discovery may have found the REAL app.
124
+ if ('unrecognised' === here.kind) {
125
+ // A monorepo root naming no framework is not an unrecognised APP — the app is a directory down
126
+ // and discovery did not surface it. Reporting MANIFEST_UNRECOGNISED here would read as "add
127
+ // this framework to the table" about a manifest that describes no framework at all.
128
+ return {
129
+ stackUnknownReason: here.declaresWorkspaces
130
+ ? StackUnknownReason.WORKSPACE_ROOT_NO_APPS
131
+ : StackUnknownReason.MANIFEST_UNRECOGNISED,
132
+ };
133
+ }
134
+ return { stackUnknownReason: StackUnknownReason.NO_APP_FOUND };
94
135
  }
95
136
  /**
96
137
  * The three filesystem methods `findWorkspaceApps` needs, rooted at `cwd`.
@@ -150,26 +191,16 @@ function fileExists(path) {
150
191
  * client's own claim about itself, while an env-var table is a guess that silently rots every time a
151
192
  * vendor renames a variable. Unset (a CLI run with no MCP peer) simply reports no client.
152
193
  */
153
- let clientNameHook;
194
+ /** Kept as the telemetry-facing name; the mechanism itself lives in mcp/client-identity.ts. */
154
195
  export function setMcpClientNameHook(hook) {
155
- clientNameHook = hook;
196
+ setMcpClientIdentityHook(hook);
156
197
  }
157
- /**
158
- * The client's own name and version from its handshake.
159
- *
160
- * Note what is NOT here and cannot be: the MODEL. MCP's `clientInfo` carries a name, a title and a
161
- * version and has no concept of a model, so the transport genuinely cannot tell us. The agent
162
- * self-reports it on the feedback itself, which is the only mechanism available and a reliable one
163
- * given the report is already something the agent authored.
164
- */
165
198
  function detectClient() {
166
199
  try {
167
- const info = clientNameHook?.();
168
- const name = info?.name;
169
- const version = info?.version;
200
+ const info = mcpClientIdentity();
170
201
  return {
171
- ...(name !== undefined && name !== '' ? { client: name.slice(0, 64) } : {}),
172
- ...(version !== undefined && version !== '' ? { clientVersion: version.slice(0, 32) } : {}),
202
+ ...(info.name === undefined ? {} : { client: info.name }),
203
+ ...(info.version === undefined ? {} : { clientVersion: info.version }),
173
204
  };
174
205
  }
175
206
  catch {
@@ -0,0 +1,15 @@
1
+ import type { Feedback } from '@reticlehq/core';
2
+ export interface SavedFeedback {
3
+ path: string;
4
+ /** A ready-to-run command that turns the file into an issue. */
5
+ command: string;
6
+ }
7
+ /**
8
+ * Write the report under `<cwd>/.reticle/feedback/`. Returns undefined if it cannot be written —
9
+ * a feedback save that throws would take down the tool call that was reporting a problem, which is
10
+ * the one outcome worse than losing the report.
11
+ */
12
+ export declare function saveFeedbackLocally(cwd: string, input: {
13
+ kind?: string;
14
+ source?: string;
15
+ }, text: string, context: Partial<Feedback>, now?: number): SavedFeedback | undefined;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Where a feedback report goes when it cannot be sent.
3
+ *
4
+ * The report is fully written and redacted by the time delivery is refused. Throwing it away
5
+ * punishes exactly the behaviour the channel exists to encourage — and it hits hardest where the
6
+ * reports are best, because a Reticle source checkout disables telemetry by cwd, so the contributor
7
+ * who can name the file and the line is the one person who cannot file at all.
8
+ *
9
+ * Writing it down costs nothing and loses nothing. The receipt then carries a path and a command,
10
+ * so the human is handed one thing to run instead of being asked to retype what an agent already
11
+ * wrote.
12
+ */
13
+ import { mkdirSync, writeFileSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ import { ReticleDir } from '@reticlehq/core';
16
+ /** Reports live beside the rest of the workspace, not in a temp dir somebody has to be told about. */
17
+ const FEEDBACK_SUBDIR = 'feedback';
18
+ /** Filesystem-safe, sortable, and unique enough for one report per second per kind. */
19
+ const stamp = (now) => new Date(now).toISOString().replace(/[:.]/g, '-').replace('Z', '');
20
+ const asMarkdown = (input, text, context) => {
21
+ const title = `Reticle feedback: ${input.kind ?? 'note'}`;
22
+ return [
23
+ `# ${title}`,
24
+ '',
25
+ text.trim(),
26
+ '',
27
+ '## Context',
28
+ '',
29
+ '```json',
30
+ JSON.stringify(context, null, 2),
31
+ '```',
32
+ '',
33
+ ].join('\n');
34
+ };
35
+ /**
36
+ * Write the report under `<cwd>/.reticle/feedback/`. Returns undefined if it cannot be written —
37
+ * a feedback save that throws would take down the tool call that was reporting a problem, which is
38
+ * the one outcome worse than losing the report.
39
+ */
40
+ export function saveFeedbackLocally(cwd, input, text, context, now = Date.now()) {
41
+ try {
42
+ const dir = join(cwd, ReticleDir.ROOT, FEEDBACK_SUBDIR);
43
+ mkdirSync(dir, { recursive: true });
44
+ const path = join(dir, `${stamp(now)}-${input.kind ?? 'note'}.md`);
45
+ writeFileSync(path, asMarkdown(input, text, context), 'utf8');
46
+ return {
47
+ path,
48
+ // `--body-file` rather than an inlined body: the report contains newlines, quotes and JSON,
49
+ // and a command somebody has to repair before running is a command they do not run.
50
+ command: `gh issue create --repo reticlehq/reticle --title "Reticle feedback: ${input.kind ?? 'note'}" --body-file "${path}"`,
51
+ };
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ }
@@ -55,6 +55,17 @@ export interface FeedbackReceipt {
55
55
  */
56
56
  accepted: boolean;
57
57
  reason?: string;
58
+ /**
59
+ * Where the report was written when it could not be SENT.
60
+ *
61
+ * A refusal used to be the end of the road: the report was already written by then, and throwing
62
+ * it away punished exactly the behaviour this channel exists to encourage. Worse, the commonest
63
+ * refusal is a Reticle SOURCE CHECKOUT — so the reporter best placed to name a file and a line was
64
+ * the only one who could not file at all.
65
+ */
66
+ savedTo?: string;
67
+ /** A ready-to-run command that turns the saved file into an issue. */
68
+ fileWith?: string;
58
69
  /** Which redaction rules fired. Empty when nothing needed removing. */
59
70
  redacted: string[];
60
71
  /** The exact context that went with it, echoed back so the send is never a black box. */
@@ -26,6 +26,7 @@ import { platform } from 'node:os';
26
26
  import { getTelemetry } from './telemetry.js';
27
27
  import { noteFeedbackUndelivered } from './feedback-delivery.js';
28
28
  import { isReticleSourceCheckout } from './dev-repo.js';
29
+ import { saveFeedbackLocally } from './feedback-local.js';
29
30
  import { SERVER_VERSION } from '../version/server-version.js';
30
31
  import { feedbackContext } from './feedback-context.js';
31
32
  import { markDelivered, outboxPath, queueFeedback } from './feedback-outbox.js';
@@ -140,15 +141,27 @@ export async function submitFeedback(input, opts = {}) {
140
141
  }
141
142
  const telemetry = getTelemetry();
142
143
  if (!telemetry.enabled) {
144
+ /*
145
+ * Cannot SEND is not the same as cannot FILE.
146
+ *
147
+ * The report is fully written and redacted by this point. Discarding it punishes the exact
148
+ * behaviour this channel exists to encourage, and it hits hardest where the reports are best: a
149
+ * Reticle source checkout disables telemetry by cwd, so a contributor who can name the file and
150
+ * the line was the one person who could not file at all.
151
+ *
152
+ * So it goes to disk and the receipt says where, plus a command that turns it into an issue.
153
+ * Nothing is lost, and the human is handed one thing to run rather than asked to retype it.
154
+ */
155
+ const cwd = opts.cwd ?? process.cwd();
156
+ const saved = saveFeedbackLocally(cwd, input, body.text, context);
143
157
  return {
144
158
  sent: false,
145
159
  accepted: false,
146
- // Named precisely, because the commonest cause is not what the old wording suggested: a
147
- // Reticle SOURCE CHECKOUT disables telemetry by cwd, and that is exactly where release runs and
148
- // contributor sessions happen — so the reports most worth having were the ones silently lost.
149
- reason: isReticleSourceCheckout(opts.cwd ?? process.cwd())
150
- ? 'this is a Reticle source checkout, where telemetry is disabled by design, so feedback has nowhere to go. Open an issue at https://github.com/reticlehq/reticle/issues instead, or run from the app you are verifying.'
151
- : `telemetry is disabled on this machine, so feedback has nowhere to go. Re-enable with \`reticle telemetry enable\`, or open an issue at https://github.com/reticlehq/reticle/issues.`,
160
+ // Named precisely, because the commonest cause is not what the old wording suggested.
161
+ reason: isReticleSourceCheckout(cwd)
162
+ ? 'this is a Reticle source checkout, where telemetry is disabled by design, so this could not be sent — it has been written to disk instead, and nothing was lost.'
163
+ : `telemetry is disabled on this machine, so this could not be sent — it has been written to disk instead. Re-enable with \`reticle telemetry enable\` to send directly.`,
164
+ ...(saved === undefined ? {} : { savedTo: saved.path, fileWith: saved.command }),
152
165
  redacted,
153
166
  context,
154
167
  };
@@ -15,6 +15,8 @@ import { type InitOutcome } from '@reticlehq/core';
15
15
  export declare const InitFailure: {
16
16
  /** Run outside a project root. The single most common first-run mistake. */
17
17
  readonly NO_PACKAGE_JSON: "no_package_json";
18
+ /** The manifest exists and is not valid JSON — a trailing comma, usually. Never a stack trace. */
19
+ readonly MALFORMED_PACKAGE_JSON: "malformed_package_json";
18
20
  /** The package manager failed: offline, a locked registry, a broken install. */
19
21
  readonly DEPENDENCY_INSTALL: "dependency_install";
20
22
  /** The `claude mcp add` step failed — the CLI is missing or refused. Reticle installs but is unreachable. */
@@ -17,6 +17,8 @@ import { resolveInstallSource } from './install-source.js';
17
17
  export const InitFailure = {
18
18
  /** Run outside a project root. The single most common first-run mistake. */
19
19
  NO_PACKAGE_JSON: 'no_package_json',
20
+ /** The manifest exists and is not valid JSON — a trailing comma, usually. Never a stack trace. */
21
+ MALFORMED_PACKAGE_JSON: 'malformed_package_json',
20
22
  /** The package manager failed: offline, a locked registry, a broken install. */
21
23
  DEPENDENCY_INSTALL: 'dependency_install',
22
24
  /** The `claude mcp add` step failed — the CLI is missing or refused. Reticle installs but is unreachable. */