@reticlehq/server 2.4.0 → 2.6.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 (472) hide show
  1. package/SKILL.md +719 -0
  2. package/dist/bridge/auth-failure-reason.d.ts +19 -0
  3. package/dist/bridge/auth-failure-reason.js +29 -0
  4. package/dist/{bridge-security.d.ts → bridge/bridge-security.d.ts} +1 -1
  5. package/dist/{bridge-security.js → bridge/bridge-security.js} +1 -1
  6. package/dist/{bridge.d.ts → bridge/bridge.d.ts} +13 -1
  7. package/dist/{bridge.js → bridge/bridge.js} +95 -15
  8. package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +9 -11
  9. package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +18 -10
  10. package/dist/{pairing-token.js → bridge/pairing-token.js} +1 -1
  11. package/dist/capsule/capsule-store.d.ts +8 -3
  12. package/dist/capsule/capsule-store.js +7 -2
  13. package/dist/capsule/capsule.js +3 -3
  14. package/dist/capsule/causal-summary.d.ts +8 -0
  15. package/dist/capsule/causal-summary.js +22 -12
  16. package/dist/capsule/divergence.d.ts +9 -4
  17. package/dist/capsule/divergence.js +8 -8
  18. package/dist/capsule/predicate-to-links.js +11 -8
  19. package/dist/cli/cli-doctor.d.ts +1 -0
  20. package/dist/cli/cli-doctor.js +135 -0
  21. package/dist/{cli-flow-commands.d.ts → cli/cli-flow-commands.d.ts} +2 -2
  22. package/dist/{cli-flow-commands.js → cli/cli-flow-commands.js} +15 -15
  23. package/dist/{cli-launch.d.ts → cli/cli-launch.d.ts} +32 -3
  24. package/dist/cli/cli-launch.js +171 -0
  25. package/dist/cli/cli-parse-feedback.d.ts +31 -0
  26. package/dist/cli/cli-parse-feedback.js +56 -0
  27. package/dist/{cli-parse.d.ts → cli/cli-parse.d.ts} +6 -19
  28. package/dist/{cli-parse.js → cli/cli-parse.js} +118 -81
  29. package/dist/{cli-port.d.ts → cli/cli-port.d.ts} +12 -0
  30. package/dist/{cli-port.js → cli/cli-port.js} +9 -9
  31. package/dist/cli/cli-update-commands.d.ts +4 -0
  32. package/dist/cli/cli-update-commands.js +93 -0
  33. package/dist/{cli-verify.js → cli/cli-verify.js} +14 -14
  34. package/dist/{cloud-cli.js → cli/cloud-cli.js} +29 -24
  35. package/dist/cli/doctor-daemon-line.d.ts +37 -0
  36. package/dist/cli/doctor-daemon-line.js +35 -0
  37. package/dist/cli/port-holder.d.ts +43 -0
  38. package/dist/cli/port-holder.js +99 -0
  39. package/dist/cli.d.ts +2 -2
  40. package/dist/cli.js +302 -100
  41. package/dist/cloud/cloud-config.d.ts +0 -1
  42. package/dist/cloud/cloud-config.js +9 -9
  43. package/dist/cloud/cloud-sync.d.ts +1 -7
  44. package/dist/cloud/cloud-sync.js +12 -12
  45. package/dist/crawl/crawl-empty.d.ts +16 -0
  46. package/dist/crawl/crawl-empty.js +24 -0
  47. package/dist/crawl/crawl-tools.js +4 -1
  48. package/dist/crawl/crawl.js +26 -7
  49. package/dist/daemon/daemon-resilience.d.ts +73 -0
  50. package/dist/daemon/daemon-resilience.js +279 -0
  51. package/dist/daemon/daemon-usefulness.d.ts +61 -0
  52. package/dist/daemon/daemon-usefulness.js +62 -0
  53. package/dist/{daemon.d.ts → daemon/daemon.d.ts} +33 -8
  54. package/dist/{daemon.js → daemon/daemon.js} +105 -48
  55. package/dist/daemon/heartbeat.d.ts +115 -0
  56. package/dist/daemon/heartbeat.js +141 -0
  57. package/dist/daemon/idle-grace.d.ts +32 -0
  58. package/dist/daemon/idle-grace.js +41 -0
  59. package/dist/{idle-shutdown.d.ts → daemon/idle-shutdown.d.ts} +23 -2
  60. package/dist/{idle-shutdown.js → daemon/idle-shutdown.js} +37 -6
  61. package/dist/daemon/port-presence.d.ts +76 -0
  62. package/dist/daemon/port-presence.js +93 -0
  63. package/dist/daemon/state-dir.d.ts +7 -0
  64. package/dist/daemon/state-dir.js +59 -0
  65. package/dist/daemon/unclean-predecessor.d.ts +23 -0
  66. package/dist/daemon/unclean-predecessor.js +27 -0
  67. package/dist/domain/domain-model.js +2 -2
  68. package/dist/events/body-failures.js +8 -8
  69. package/dist/events/contradictions.d.ts +1 -1
  70. package/dist/events/contradictions.js +60 -10
  71. package/dist/events/echo-mismatch.js +10 -10
  72. package/dist/events/event-filters.js +6 -6
  73. package/dist/events/json-salvage.d.ts +2 -1
  74. package/dist/events/json-salvage.js +6 -6
  75. package/dist/events/observed-in-window.d.ts +12 -0
  76. package/dist/events/observed-in-window.js +21 -0
  77. package/dist/events/predicate-eval.d.ts +53 -20
  78. package/dist/events/predicate-eval.js +179 -55
  79. package/dist/events/predicate-parse.d.ts +23 -0
  80. package/dist/events/predicate-parse.js +54 -0
  81. package/dist/events/predicate.d.ts +7 -2
  82. package/dist/events/predicate.js +126 -35
  83. package/dist/events/reconcile.js +5 -5
  84. package/dist/events/ring-buffer.js +2 -17
  85. package/dist/events/stale-response.js +6 -6
  86. package/dist/events/testid-near-miss.d.ts +22 -0
  87. package/dist/events/testid-near-miss.js +33 -0
  88. package/dist/events/unit-mismatch.js +8 -8
  89. package/dist/flows/affected-tools.js +19 -4
  90. package/dist/flows/affected.js +1 -1
  91. package/dist/flows/annotate-target.d.ts +1 -0
  92. package/dist/flows/annotate-target.js +20 -0
  93. package/dist/flows/annotate-tools.js +25 -3
  94. package/dist/flows/annotate.js +17 -4
  95. package/dist/flows/assertion-integrity.d.ts +2 -1
  96. package/dist/flows/assertion-tiers-store.d.ts +3 -66
  97. package/dist/flows/assertion-tiers-store.js +2 -2
  98. package/dist/flows/attributed-failure.d.ts +17 -0
  99. package/dist/flows/attributed-failure.js +20 -0
  100. package/dist/flows/buddy-status.d.ts +2 -1
  101. package/dist/flows/buddy-status.js +2 -2
  102. package/dist/flows/coverage.d.ts +3 -2
  103. package/dist/flows/coverage.js +2 -2
  104. package/dist/flows/decision.js +24 -9
  105. package/dist/flows/empty-flow.d.ts +6 -0
  106. package/dist/flows/empty-flow.js +24 -0
  107. package/dist/flows/flake-store.d.ts +0 -1
  108. package/dist/flows/flake-store.js +1 -1
  109. package/dist/flows/flake.d.ts +2 -1
  110. package/dist/flows/flake.js +2 -2
  111. package/dist/flows/flow-classify.js +12 -0
  112. package/dist/flows/flow-replay-run.js +4 -4
  113. package/dist/flows/flow-replay.d.ts +6 -8
  114. package/dist/flows/flow-replay.js +132 -48
  115. package/dist/flows/flow-report.js +1 -1
  116. package/dist/flows/flow-scope.d.ts +1 -6
  117. package/dist/flows/flow-step-runners.d.ts +26 -6
  118. package/dist/flows/flow-step-runners.js +156 -6
  119. package/dist/flows/flow-success.js +20 -15
  120. package/dist/flows/flow-tools.js +63 -21
  121. package/dist/flows/flows.js +15 -11
  122. package/dist/flows/gate.d.ts +4 -3
  123. package/dist/flows/gate.js +1 -1
  124. package/dist/flows/heal-run.d.ts +0 -8
  125. package/dist/flows/heal-run.js +5 -5
  126. package/dist/flows/heal.js +3 -3
  127. package/dist/flows/predicate-to-expect.d.ts +38 -0
  128. package/dist/flows/predicate-to-expect.js +95 -0
  129. package/dist/flows/replay.d.ts +16 -1
  130. package/dist/flows/replay.js +58 -22
  131. package/dist/flows/role-anchor-nearest.d.ts +6 -0
  132. package/dist/flows/role-anchor-nearest.js +45 -0
  133. package/dist/flows/role-drift-reason.d.ts +20 -0
  134. package/dist/flows/role-drift-reason.js +39 -0
  135. package/dist/flows/server-verify.d.ts +1 -1
  136. package/dist/flows/server-verify.js +5 -5
  137. package/dist/flows/suite-flakes.d.ts +33 -0
  138. package/dist/flows/suite-flakes.js +32 -0
  139. package/dist/flows/verify-change-tools.js +32 -10
  140. package/dist/flows/verify-change-verdict.d.ts +15 -0
  141. package/dist/flows/verify-change-verdict.js +25 -0
  142. package/dist/flows/watch-batcher.d.ts +3 -2
  143. package/dist/honesty/already-true.d.ts +2 -0
  144. package/dist/honesty/already-true.js +31 -0
  145. package/dist/honesty/blind-spots.d.ts +35 -3
  146. package/dist/honesty/blind-spots.js +57 -12
  147. package/dist/honesty/honesty.d.ts +3 -2
  148. package/dist/honesty/honesty.js +4 -4
  149. package/dist/honesty/uncaptured-bodies.js +1 -1
  150. package/dist/honesty/unread-outcome.js +2 -2
  151. package/dist/honesty/verified.d.ts +31 -3
  152. package/dist/honesty/verified.js +64 -8
  153. package/dist/http-server.js +23 -7
  154. package/dist/hunt/hunt-report.d.ts +2 -1
  155. package/dist/hunt/hunt-report.js +2 -2
  156. package/dist/index.d.ts +9 -6
  157. package/dist/index.js +41 -25
  158. package/dist/init/agent-rules.d.ts +20 -1
  159. package/dist/init/agent-rules.js +64 -8
  160. package/dist/init/app-choice.d.ts +20 -0
  161. package/dist/init/app-choice.js +25 -0
  162. package/dist/init/astro-host.d.ts +21 -0
  163. package/dist/init/astro-host.js +27 -0
  164. package/dist/init/astro-patch.d.ts +16 -0
  165. package/dist/init/astro-patch.js +240 -0
  166. package/dist/init/capabilities.js +4 -1
  167. package/dist/init/cra.d.ts +50 -0
  168. package/dist/init/cra.js +106 -0
  169. package/dist/init/cursor.js +39 -4
  170. package/dist/init/desktop-doctor.d.ts +3 -2
  171. package/dist/init/desktop-doctor.js +4 -4
  172. package/dist/init/detect.d.ts +9 -2
  173. package/dist/init/detect.js +24 -5
  174. package/dist/init/mcp-clients.d.ts +81 -0
  175. package/dist/init/mcp-clients.js +248 -0
  176. package/dist/init/mcp-registered.d.ts +12 -0
  177. package/dist/init/mcp-registered.js +14 -0
  178. package/dist/init/mcp.d.ts +27 -2
  179. package/dist/init/mcp.js +40 -4
  180. package/dist/init/next-patch.d.ts +2 -4
  181. package/dist/init/next-patch.js +109 -19
  182. package/dist/init/node-io.js +29 -6
  183. package/dist/init/plan-framework.d.ts +16 -1
  184. package/dist/init/plan-framework.js +166 -8
  185. package/dist/init/plan.d.ts +85 -6
  186. package/dist/init/plan.js +196 -33
  187. package/dist/init/project-id.d.ts +5 -9
  188. package/dist/init/project-id.js +10 -22
  189. package/dist/init/run.d.ts +23 -1
  190. package/dist/init/run.js +338 -47
  191. package/dist/init/slash-command.d.ts +9 -0
  192. package/dist/init/slash-command.js +10 -1
  193. package/dist/init/snippets.d.ts +7 -10
  194. package/dist/init/snippets.js +44 -15
  195. package/dist/init/vite-config.js +1 -1
  196. package/dist/init/windows-quote.d.ts +38 -0
  197. package/dist/init/windows-quote.js +70 -0
  198. package/dist/init/workspace-apps.d.ts +23 -0
  199. package/dist/init/workspace-apps.js +68 -0
  200. package/dist/input/driven-redaction.js +1 -1
  201. package/dist/input/network-detail.js +8 -8
  202. package/dist/input/network-mock-tools.js +2 -2
  203. package/dist/input/network-mock.js +4 -4
  204. package/dist/input/real-input.js +8 -3
  205. package/dist/input/scroll-find.js +2 -2
  206. package/dist/input/scroll-tools.d.ts +0 -5
  207. package/dist/input/scroll-tools.js +11 -4
  208. package/dist/input/viewport-tools.js +2 -2
  209. package/dist/journal/ambient-store.d.ts +0 -2
  210. package/dist/journal/ambient-store.js +1 -1
  211. package/dist/journal/ambient.js +1 -1
  212. package/dist/journal/attach-journal.d.ts +2 -1
  213. package/dist/journal/deviation-report.js +6 -6
  214. package/dist/journal/deviation-service.js +1 -1
  215. package/dist/journal/envelope-store.d.ts +0 -2
  216. package/dist/journal/envelope-store.js +1 -1
  217. package/dist/journal/envelope.d.ts +2 -3
  218. package/dist/journal/envelope.js +3 -3
  219. package/dist/journal/journal-query.js +1 -1
  220. package/dist/journal/journal-recorder.d.ts +2 -1
  221. package/dist/journal/journal-recorder.js +2 -2
  222. package/dist/journal/rollups.js +9 -9
  223. package/dist/journal/session-end.d.ts +2 -1
  224. package/dist/journal/session-journal.js +4 -4
  225. package/dist/license/license.d.ts +0 -14
  226. package/dist/license/license.js +8 -8
  227. package/dist/log.d.ts +10 -0
  228. package/dist/log.js +11 -1
  229. package/dist/mcp/mcp-is-error.d.ts +13 -0
  230. package/dist/mcp/mcp-is-error.js +18 -0
  231. package/dist/mcp/mcp-outage.d.ts +32 -0
  232. package/dist/mcp/mcp-outage.js +55 -0
  233. package/dist/mcp/mcp-proxy.d.ts +134 -0
  234. package/dist/mcp/mcp-proxy.js +720 -0
  235. package/dist/{mcp.d.ts → mcp/mcp.d.ts} +13 -10
  236. package/dist/{mcp.js → mcp/mcp.js} +139 -37
  237. package/dist/mcp/proxy-handshake.d.ts +3 -0
  238. package/dist/mcp/proxy-handshake.js +54 -0
  239. package/dist/mcp/proxy-lifecycle.d.ts +64 -0
  240. package/dist/mcp/proxy-lifecycle.js +70 -0
  241. package/dist/mcp/tool-catalog-cache.d.ts +29 -0
  242. package/dist/mcp/tool-catalog-cache.js +55 -0
  243. package/dist/oracles/propose-consequences.d.ts +2 -1
  244. package/dist/oracles/propose-consequences.js +16 -11
  245. package/dist/oracles/self-instrument.d.ts +2 -1
  246. package/dist/oracles/self-instrument.js +1 -1
  247. package/dist/phenomena/phenomena.js +3 -3
  248. package/dist/platform.d.ts +16 -0
  249. package/dist/platform.js +16 -0
  250. package/dist/pool/browser-pool.d.ts +9 -0
  251. package/dist/pool/browser-pool.js +38 -9
  252. package/dist/pool/playwright-launcher.d.ts +15 -2
  253. package/dist/pool/playwright-launcher.js +21 -2
  254. package/dist/project/fs-port.js +1 -1
  255. package/dist/project/project-tools.js +2 -2
  256. package/dist/runs/build-verification-run.js +1 -1
  257. package/dist/runs/repair-prompt.d.ts +0 -6
  258. package/dist/runs/repair-prompt.js +3 -3
  259. package/dist/runs/risk-classify.js +1 -1
  260. package/dist/runs/run-diff.d.ts +0 -2
  261. package/dist/runs/run-diff.js +3 -3
  262. package/dist/runs/run-tools.js +2 -2
  263. package/dist/runs/verification-sync.js +2 -2
  264. package/dist/runs/verify-http.js +1 -1
  265. package/dist/runs/verify-server.js +1 -1
  266. package/dist/session/attachment-history.d.ts +46 -0
  267. package/dist/session/attachment-history.js +65 -0
  268. package/dist/session/command-timeout.d.ts +11 -3
  269. package/dist/session/command-timeout.js +2 -2
  270. package/dist/session/control-envelope.d.ts +6 -0
  271. package/dist/session/control-envelope.js +7 -1
  272. package/dist/session/dev-server-probe.d.ts +13 -0
  273. package/dist/session/dev-server-probe.js +52 -0
  274. package/dist/session/last-act.d.ts +17 -3
  275. package/dist/session/last-act.js +16 -4
  276. package/dist/session/live-control-tools.js +7 -2
  277. package/dist/session/live-control.js +1 -1
  278. package/dist/session/looks-like-dev-server.d.ts +14 -0
  279. package/dist/session/looks-like-dev-server.js +25 -0
  280. package/dist/session/no-session-diagnosis.d.ts +45 -0
  281. package/dist/session/no-session-diagnosis.js +120 -0
  282. package/dist/session/no-session-watch.d.ts +21 -0
  283. package/dist/session/no-session-watch.js +69 -0
  284. package/dist/session/observed-state.d.ts +4 -0
  285. package/dist/session/observed-state.js +19 -1
  286. package/dist/session/output-budget.js +1 -1
  287. package/dist/session/pending-commands.js +1 -0
  288. package/dist/session/session-health.d.ts +8 -1
  289. package/dist/session/session-health.js +8 -7
  290. package/dist/session/session-info.d.ts +43 -8
  291. package/dist/session/session-info.js +46 -1
  292. package/dist/session/session-lease.d.ts +0 -11
  293. package/dist/session/session-lease.js +1 -1
  294. package/dist/session/session-manager.d.ts +6 -0
  295. package/dist/session/session-manager.js +124 -12
  296. package/dist/session/session-readiness.d.ts +1 -1
  297. package/dist/session/session-readiness.js +6 -1
  298. package/dist/session/session-reaper.js +1 -1
  299. package/dist/session/session-reconnect.d.ts +32 -0
  300. package/dist/session/session-reconnect.js +42 -0
  301. package/dist/session/session.d.ts +15 -2
  302. package/dist/session/session.js +77 -26
  303. package/dist/session/state-select.d.ts +1 -1
  304. package/dist/session/state-select.js +1 -1
  305. package/dist/status-payload.d.ts +19 -0
  306. package/dist/status-payload.js +11 -0
  307. package/dist/telemetry/argument-shape.js +10 -1
  308. package/dist/telemetry/browser-mode.d.ts +27 -0
  309. package/dist/telemetry/browser-mode.js +33 -0
  310. package/dist/telemetry/bug-found.d.ts +17 -21
  311. package/dist/telemetry/bug-found.js +120 -8
  312. package/dist/telemetry/cli-telemetry.d.ts +2 -12
  313. package/dist/telemetry/cli-telemetry.js +27 -4
  314. package/dist/telemetry/crash-cause.d.ts +47 -0
  315. package/dist/telemetry/crash-cause.js +104 -0
  316. package/dist/telemetry/daemon-telemetry.d.ts +38 -1
  317. package/dist/telemetry/daemon-telemetry.js +37 -12
  318. package/dist/telemetry/dev-repo.js +4 -4
  319. package/dist/telemetry/error-class.d.ts +26 -0
  320. package/dist/telemetry/error-class.js +52 -0
  321. package/dist/telemetry/error-fingerprint.js +21 -2
  322. package/dist/telemetry/feedback-cli.d.ts +8 -3
  323. package/dist/telemetry/feedback-cli.js +52 -8
  324. package/dist/telemetry/feedback-context.d.ts +23 -6
  325. package/dist/telemetry/feedback-context.js +105 -5
  326. package/dist/telemetry/feedback-delivery.d.ts +20 -0
  327. package/dist/telemetry/feedback-delivery.js +30 -0
  328. package/dist/telemetry/feedback-outbox.d.ts +22 -0
  329. package/dist/telemetry/feedback-outbox.js +87 -0
  330. package/dist/telemetry/feedback.d.ts +22 -1
  331. package/dist/telemetry/feedback.js +67 -8
  332. package/dist/telemetry/git-facts.d.ts +7 -0
  333. package/dist/telemetry/git-facts.js +5 -5
  334. package/dist/telemetry/project-profile.js +2 -1
  335. package/dist/telemetry/session-metrics.d.ts +47 -3
  336. package/dist/telemetry/session-metrics.js +315 -15
  337. package/dist/telemetry/telemetry.d.ts +45 -2
  338. package/dist/telemetry/telemetry.js +150 -37
  339. package/dist/telemetry/verification-of.d.ts +22 -0
  340. package/dist/telemetry/verification-of.js +84 -0
  341. package/dist/tools/act-capsule.js +2 -2
  342. package/dist/tools/act-danger.d.ts +12 -0
  343. package/dist/tools/act-danger.js +20 -3
  344. package/dist/tools/act-helpers.js +4 -4
  345. package/dist/tools/act-tools.d.ts +0 -8
  346. package/dist/tools/act-tools.js +114 -118
  347. package/dist/tools/act-view.js +7 -7
  348. package/dist/tools/alias-args.d.ts +17 -0
  349. package/dist/tools/alias-args.js +25 -0
  350. package/dist/tools/assert-grade.js +9 -9
  351. package/dist/tools/assert-verdict.d.ts +9 -1
  352. package/dist/tools/assert-verdict.js +21 -6
  353. package/dist/tools/browser-tools.js +36 -11
  354. package/dist/tools/contract-tools.d.ts +0 -5
  355. package/dist/tools/contract-tools.js +27 -1
  356. package/dist/tools/coverage-identity.d.ts +34 -0
  357. package/dist/tools/coverage-identity.js +46 -0
  358. package/dist/tools/coverage-tools.js +10 -14
  359. package/dist/tools/dynamic-tools.d.ts +2 -1
  360. package/dist/tools/dynamic-tools.js +96 -19
  361. package/dist/tools/error-recovery.d.ts +34 -2
  362. package/dist/tools/error-recovery.js +255 -4
  363. package/dist/tools/feedback-invite.d.ts +48 -0
  364. package/dist/tools/feedback-invite.js +71 -0
  365. package/dist/tools/feedback-tools.d.ts +12 -1
  366. package/dist/tools/feedback-tools.js +28 -5
  367. package/dist/tools/invoke-tool.d.ts +0 -12
  368. package/dist/tools/invoke-tool.js +126 -33
  369. package/dist/tools/lease-hint.d.ts +17 -0
  370. package/dist/tools/lease-hint.js +24 -0
  371. package/dist/tools/lease-tools.js +6 -5
  372. package/dist/tools/merge-tools.d.ts +10 -1
  373. package/dist/tools/merge-tools.js +9 -3
  374. package/dist/tools/merged-name-redirect.d.ts +13 -0
  375. package/dist/tools/merged-name-redirect.js +55 -0
  376. package/dist/tools/navigate-arrival.d.ts +26 -0
  377. package/dist/tools/navigate-arrival.js +60 -0
  378. package/dist/tools/navigate-result.d.ts +31 -0
  379. package/dist/tools/navigate-result.js +38 -0
  380. package/dist/tools/observe-tools.js +72 -20
  381. package/dist/tools/observed-nothing.d.ts +25 -0
  382. package/dist/tools/observed-nothing.js +33 -0
  383. package/dist/tools/query-paginate.js +2 -2
  384. package/dist/tools/query-shape.d.ts +18 -0
  385. package/dist/tools/query-shape.js +39 -0
  386. package/dist/tools/read-tools.js +81 -20
  387. package/dist/tools/real-input-attempt.d.ts +28 -0
  388. package/dist/tools/real-input-attempt.js +93 -0
  389. package/dist/tools/reconcile-tools.js +7 -7
  390. package/dist/tools/reload-result.d.ts +14 -0
  391. package/dist/tools/reload-result.js +29 -0
  392. package/dist/tools/snapshot-delta.d.ts +4 -5
  393. package/dist/tools/snapshot-delta.js +0 -0
  394. package/dist/tools/tool-kit.d.ts +16 -0
  395. package/dist/tools/tool-surface.d.ts +83 -0
  396. package/dist/tools/tool-surface.js +217 -0
  397. package/dist/tools/tools-helpers.js +6 -6
  398. package/dist/tools/tools.d.ts +15 -0
  399. package/dist/tools/tools.js +53 -17
  400. package/dist/tools/unadvertised-help.d.ts +5 -0
  401. package/dist/tools/unadvertised-help.js +39 -0
  402. package/dist/trace.d.ts +69 -0
  403. package/dist/trace.js +150 -0
  404. package/dist/update/nudge-credit.d.ts +4 -0
  405. package/dist/update/nudge-credit.js +53 -0
  406. package/dist/update/sdk-sync.d.ts +21 -0
  407. package/dist/update/sdk-sync.js +42 -0
  408. package/dist/update/update-checker.js +3 -3
  409. package/dist/update/update-nudge.d.ts +63 -6
  410. package/dist/update/update-nudge.js +128 -15
  411. package/dist/update/updater.d.ts +1 -9
  412. package/dist/update/updater.js +13 -8
  413. package/dist/version/peer-announce.d.ts +4 -0
  414. package/dist/version/peer-announce.js +30 -0
  415. package/dist/{server-version.js → version/server-version.js} +5 -1
  416. package/dist/version/version-nudge.d.ts +28 -0
  417. package/dist/version/version-nudge.js +40 -0
  418. package/dist/version/version-skew.d.ts +66 -0
  419. package/dist/version/version-skew.js +71 -0
  420. package/dist/visual/visual-diff.js +2 -2
  421. package/dist/visual/visual-tools.js +4 -4
  422. package/docs/README.md +55 -0
  423. package/docs/agent-cheatsheet.md +85 -0
  424. package/docs/architecture.md +113 -0
  425. package/docs/benchmarks.md +255 -0
  426. package/docs/debugging.md +102 -0
  427. package/docs/deploy-checks.md +69 -0
  428. package/docs/desktop-apps.md +256 -0
  429. package/docs/enterprise.md +62 -0
  430. package/docs/fixtures-dispatch-receiver.yml +76 -0
  431. package/docs/fixtures.md +45 -0
  432. package/docs/flows.md +166 -0
  433. package/docs/gate-plan.md +98 -0
  434. package/docs/gates.md +117 -0
  435. package/docs/getting-started.md +469 -0
  436. package/docs/human-control.md +63 -0
  437. package/docs/integration-patterns.md +171 -0
  438. package/docs/local-registry.md +126 -0
  439. package/docs/matrix/2.5.0/claude-code-machine.json +19 -0
  440. package/docs/matrix/2.5.0/codex-machine.json +20 -0
  441. package/docs/matrix/2.5.0/cursor-machine.json +22 -0
  442. package/docs/matrix/2.5.0/gemini-machine.json +22 -0
  443. package/docs/matrix/2.5.0/opencode-machine.json +22 -0
  444. package/docs/matrix/2.5.0/vscode-machine.json +22 -0
  445. package/docs/matrix/2.5.0/windsurf-machine.json +22 -0
  446. package/docs/matrix/MATRIX.md +26 -0
  447. package/docs/matrix/README.md +59 -0
  448. package/docs/mint.json +61 -0
  449. package/docs/multi-agent-testing.md +55 -0
  450. package/docs/platform-integration.md +122 -0
  451. package/docs/system-map.md +152 -0
  452. package/docs/telemetry-contract.md +214 -0
  453. package/docs/telemetry.md +191 -0
  454. package/docs/testing.md +82 -0
  455. package/docs/token-efficiency.md +80 -0
  456. package/docs/usage.md +1046 -0
  457. package/package.json +8 -5
  458. package/dist/cli-doctor.d.ts +0 -6
  459. package/dist/cli-doctor.js +0 -60
  460. package/dist/cli-launch.js +0 -112
  461. package/dist/daemon-resilience.d.ts +0 -22
  462. package/dist/daemon-resilience.js +0 -85
  463. package/dist/mcp-proxy.d.ts +0 -67
  464. package/dist/mcp-proxy.js +0 -337
  465. package/dist/tools/profiles.d.ts +0 -37
  466. package/dist/tools/profiles.js +0 -139
  467. /package/dist/{pairing-token.d.ts → bridge/pairing-token.d.ts} +0 -0
  468. /package/dist/{token-auth.d.ts → bridge/token-auth.d.ts} +0 -0
  469. /package/dist/{token-auth.js → bridge/token-auth.js} +0 -0
  470. /package/dist/{cli-verify.d.ts → cli/cli-verify.d.ts} +0 -0
  471. /package/dist/{cloud-cli.d.ts → cli/cloud-cli.d.ts} +0 -0
  472. /package/dist/{server-version.d.ts → version/server-version.d.ts} +0 -0
package/dist/cli.js CHANGED
@@ -2,30 +2,35 @@
2
2
  import { pathToFileURL } from 'node:url';
3
3
  import { realpathSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
- import { handleWatch, handleCapsules, handleGate, loadNamedFlows, resolveChangedFiles, } from './cli-flow-commands.js';
5
+ import { stateDirProblem } from './daemon/state-dir.js';
6
+ import { reticleStateHome } from './daemon/daemon.js';
7
+ import { handleWatch, handleCapsules, handleGate, loadNamedFlows, resolveChangedFiles, } from './cli/cli-flow-commands.js';
6
8
  import { RETICLE_DEFAULT_PORT, ReticleDir, ReticleEnv } from '@reticlehq/core';
7
9
  import { loadDotEnv } from './telemetry/dev-repo.js';
8
10
  import { createNodeFileSystem } from './project/fs-port.js';
9
11
  import { affectedSavedFlows } from './flows/flow-sources.js';
10
- import { checkForUpdate } from './update/update-checker.js';
11
- import { applyUpdate, rollback } from './update/updater.js';
12
+ import { availableUpdate } from './update/update-nudge.js';
13
+ import { handleUpdate, handleRollback } from './cli/cli-update-commands.js';
12
14
  import { start, startDaemon } from './index.js';
13
- import { isCloudCommand, runCloudCommand } from './cloud-cli.js';
14
- import { SERVER_VERSION } from './server-version.js';
15
+ import { isCloudCommand, runCloudCommand } from './cli/cloud-cli.js';
16
+ import { SERVER_VERSION } from './version/server-version.js';
15
17
  import { log } from './log.js';
16
- import { readPid, isAlive, isRunning, removePid, spawnDaemon, discoverDaemonPort, writeDaemonRegistry, } from './daemon.js';
17
- import { waitForDaemon, startMcpProxy, probeDaemon } from './mcp-proxy.js';
18
- import { installDaemonResilience } from './daemon-resilience.js';
19
- import { IdleShutdown, resolveIdleShutdownMs } from './idle-shutdown.js';
20
- import { fetchStatus, summarizeStatus, decideOpen, openInBrowser } from './cli-launch.js';
21
- import { handleVerify } from './cli-verify.js';
18
+ import { readPid, isAlive, removePid, spawnDaemon, discoverDaemonPort, writeDaemonRegistry, logPath, } from './daemon/daemon.js';
19
+ import { PortPresence, probePresence, presenceIsUsable, describePresence, } from './daemon/port-presence.js';
20
+ import { waitForDaemon, startMcpProxy, probeDaemon, proxyLog, setProxyLogPort, } from './mcp/mcp-proxy.js';
21
+ import { installDaemonResilience, installProxyResilience, recordExitReason, DaemonExitReason, } from './daemon/daemon-resilience.js';
22
+ import { IdleShutdown, resolveIdleShutdownMs, resolveIdleCheckMs } from './daemon/idle-shutdown.js';
23
+ import { DaemonHeartbeat, resolveHeartbeatMs } from './daemon/heartbeat.js';
24
+ import { everServedToolCall } from './daemon/daemon-usefulness.js';
25
+ import { fetchStatus, summarizeStatus, warnOnDaemonSkew, decideOpen, openInBrowser, } from './cli/cli-launch.js';
26
+ import { handleVerify } from './cli/cli-verify.js';
22
27
  import { summarizeHunt } from './hunt/hunt-report.js';
23
28
  import { runInit } from './init/run.js';
24
- import { handleDoctor } from './cli-doctor.js';
29
+ import { handleDoctor } from './cli/cli-doctor.js';
25
30
  import { buildNodeIo } from './init/node-io.js';
26
31
  import { describeLicense } from './license/license.js';
27
- import { isLikelyDevServerPort, devServerPortWarning, readProjectPort, readProjectId } from './cli-port.js';
28
- import { DAEMON_INNER_COMMAND, PORT_FLAG, DRIVE_FLAG, HEADED_FLAG, HTTP_FLAG, HTTP_PORT_FLAG, HTTP_TOKEN_FLAG, parseCliArgs, CLI_USAGE, } from './cli-parse.js';
32
+ import { isLikelyDevServerPort, devServerPortWarning, readProjectPort, readProjectId, } from './cli/cli-port.js';
33
+ import { DAEMON_INNER_COMMAND, PORT_FLAG, DRIVE_FLAG, HEADED_FLAG, HTTP_FLAG, HTTP_PORT_FLAG, HTTP_TOKEN_FLAG, parseCliArgs, CLI_USAGE, } from './cli/cli-parse.js';
29
34
  import { handleFeedback, handleIdentify, handleTelemetry } from './telemetry/feedback-cli.js';
30
35
  import { installDaemonTelemetry } from './telemetry/daemon-telemetry.js';
31
36
  import { reportCliRun } from './telemetry/cli-telemetry.js';
@@ -33,15 +38,50 @@ import { reportCliRun } from './telemetry/cli-telemetry.js';
33
38
  export { parseCliArgs, CLI_USAGE };
34
39
  function handleInit(parsed) {
35
40
  const cwd = process.cwd();
36
- const result = runInit({ cwd, port: parsed.port, mcp: parsed.mcp, dryRun: parsed.dryRun, install: parsed.install }, buildNodeIo(cwd));
41
+ const result = runInit({
42
+ cwd,
43
+ port: parsed.port,
44
+ mcp: parsed.mcp,
45
+ dryRun: parsed.dryRun,
46
+ install: parsed.install,
47
+ ...(parsed.app === undefined ? {} : { app: parsed.app }),
48
+ }, buildNodeIo(cwd));
37
49
  if (!result.ok)
38
50
  process.exit(1);
39
51
  }
40
52
  function handleServe(parsed) {
41
- if (isRunning(parsed.port)) {
53
+ void serveWithHonestExit(parsed);
54
+ }
55
+ /**
56
+ * Report the BIND, not the spawn.
57
+ *
58
+ * `serve` used to log `reticle_daemon_spawned` and exit 0 unconditionally, while the child died at
59
+ * `reticle_daemon_start_failed` with an EADDRINUSE nothing joined back to the parent. Three surfaces
60
+ * then disagreed about what was true and none of them named the port.
61
+ *
62
+ * Two changes, both about saying what is actually the case: refuse up front when the port is held by
63
+ * something that is not a Reticle daemon (a spawn there cannot succeed), and after spawning, wait
64
+ * for the daemon to actually answer before claiming it started.
65
+ */
66
+ async function serveWithHonestExit(parsed) {
67
+ const presence = await probePresence(parsed.port, {
68
+ tcpOpen: probeDaemon,
69
+ status: fetchStatus,
70
+ });
71
+ if (presence === PortPresence.DAEMON) {
42
72
  log('reticle_daemon_already_running', { port: parsed.port });
43
73
  return;
44
74
  }
75
+ if (presence === PortPresence.FOREIGN) {
76
+ log('reticle_daemon_start_refused', {
77
+ port: parsed.port,
78
+ presence,
79
+ reason: describePresence(presence, parsed.port),
80
+ });
81
+ process.stderr.write(`${describePresence(presence, parsed.port)}\n`);
82
+ process.exit(1);
83
+ return;
84
+ }
45
85
  const scriptPath = process.argv[1];
46
86
  if (scriptPath === undefined) {
47
87
  log('reticle_serve_no_script', {});
@@ -62,11 +102,50 @@ function handleServe(parsed) {
62
102
  daemonArgs.push(HTTP_TOKEN_FLAG, parsed.httpToken);
63
103
  }
64
104
  spawnDaemon(process.execPath, scriptPath, daemonArgs, parsed.port);
105
+ // The child binds asynchronously and, when it cannot, exits 1 long after this process would have
106
+ // reported success. Wait for it to ANSWER — `/status` responding is the only evidence a daemon
107
+ // exists that does not come from the pid file the child may never have earned.
108
+ const bound = await waitForPresence(parsed.port, PortPresence.DAEMON, SERVE_BIND_TIMEOUT_MS);
109
+ if (!bound) {
110
+ const settled = await probePresence(parsed.port, { tcpOpen: probeDaemon, status: fetchStatus });
111
+ log('reticle_daemon_start_failed_parent', {
112
+ port: parsed.port,
113
+ presence: settled,
114
+ reason: describePresence(settled, parsed.port),
115
+ log: logPath(parsed.port),
116
+ stateDir: stateDirProblem(reticleStateHome()) === undefined ? 'writable' : 'unwritable',
117
+ });
118
+ // Name the CAUSE when we can see it. An unwritable state directory is a first-run failure mode
119
+ // (locked-down home, read-only container mount, managed profile) that produced only "nothing is
120
+ // listening" — the symptom restated — and then pointed at a log INSIDE that directory, which
121
+ // cannot exist. Found by stress-testing 2.6.0 with `chmod 555 ~/.reticle`.
122
+ const dirProblem = stateDirProblem(reticleStateHome());
123
+ process.stderr.write(dirProblem === undefined
124
+ ? `the daemon did not come up on :${String(parsed.port)} — ${describePresence(settled, parsed.port)}\n` +
125
+ `see ${logPath(parsed.port)}\n`
126
+ : `the daemon did not come up on :${String(parsed.port)}.\n${dirProblem}\n`);
127
+ process.exit(1);
128
+ return;
129
+ }
65
130
  log('reticle_daemon_spawned', { port: parsed.port, ...(parsed.http ? { http: true } : {}) });
66
131
  }
132
+ /** How long `serve` waits for the child to bind. Generous: a cold daemon start is seconds. */
133
+ const SERVE_BIND_TIMEOUT_MS = 15_000;
134
+ const PRESENCE_POLL_MS = 150;
135
+ async function waitForPresence(port, want, timeoutMs) {
136
+ const deadline = Date.now() + timeoutMs;
137
+ for (;;) {
138
+ const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
139
+ if (presence === want)
140
+ return true;
141
+ if (Date.now() >= deadline)
142
+ return false;
143
+ await new Promise((resolve) => setTimeout(resolve, PRESENCE_POLL_MS));
144
+ }
145
+ }
67
146
  function handleStop(port, quiet) {
68
147
  const pid = readPid(port);
69
- if (pid === null || !isAlive(pid)) {
148
+ if (null === pid || !isAlive(pid)) {
70
149
  removePid(port);
71
150
  if (!quiet)
72
151
  log('reticle_daemon_not_running', { port });
@@ -92,17 +171,31 @@ function handleStop(port, quiet) {
92
171
  }
93
172
  function handleStatus(port) {
94
173
  const pid = readPid(port);
95
- if (pid === null || !isAlive(pid)) {
96
- log('reticle_status', { port, running: false });
174
+ if (null === pid || !isAlive(pid)) {
175
+ // `running: false` on its own has been reported about a port that was demonstrably occupied,
176
+ // because the pid file is not the port. Ask the port before answering.
177
+ void probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus }).then((presence) => {
178
+ log('reticle_status', {
179
+ port,
180
+ running: presenceIsUsable(presence),
181
+ presence,
182
+ ...(presence === PortPresence.FOREIGN ? { reason: describePresence(presence, port) } : {}),
183
+ });
184
+ });
97
185
  return;
98
186
  }
99
187
  // The daemon is up — ask it for live sessions + health so status is at-a-glance, not just a pid.
100
188
  void fetchStatus(port).then((payload) => {
189
+ // `status` is the second-most-run command and the one a HUMAN types. The update nudge otherwise
190
+ // only rides a tool result, which the majority of daemons never produce — so the people most in
191
+ // need of an upgrade were the ones with no path to hearing about it.
192
+ const update = availableUpdate();
193
+ const nudge = update === undefined ? {} : { updateAvailable: update };
101
194
  if (payload === undefined) {
102
- log('reticle_status', { port, running: true, pid });
195
+ log('reticle_status', { port, running: true, pid, ...nudge });
103
196
  return;
104
197
  }
105
- log('reticle_status', { port, running: true, pid, ...summarizeStatus(payload) });
198
+ log('reticle_status', { port, running: true, pid, ...summarizeStatus(payload), ...nudge });
106
199
  });
107
200
  }
108
201
  /** Print the running package version (resolved once in server-version.ts). */
@@ -172,43 +265,15 @@ async function handleHunt(dir) {
172
265
  });
173
266
  }
174
267
  }
175
- /** `reticle update` — install the latest server version and restart (moved off the MCP surface). */
176
- async function handleUpdate() {
177
- try {
178
- const manifest = await checkForUpdate(SERVER_VERSION, () => Date.now());
179
- if (!manifest.updateAvailable || manifest.latestVersion === undefined) {
180
- log('reticle_update', {
181
- ok: false,
182
- message: 'already on the latest version',
183
- version: SERVER_VERSION,
184
- });
185
- return;
186
- }
187
- log('reticle_update', { ok: true, from: SERVER_VERSION, to: manifest.latestVersion });
188
- await applyUpdate(manifest.latestVersion); // calls process.exit; Claude Code restarts
189
- }
190
- catch (error) {
191
- log('reticle_update_failed', { error: error instanceof Error ? error.message : String(error) });
192
- process.exitCode = 1;
193
- }
194
- }
195
- /** `reticle rollback` — restore the previous server version and restart. */
196
- async function handleRollback() {
197
- try {
198
- await rollback(); // calls process.exit
199
- }
200
- catch (error) {
201
- log('reticle_rollback_failed', {
202
- error: error instanceof Error ? error.message : String(error),
203
- });
204
- process.exitCode = 1;
205
- }
206
- }
207
268
  /** Ensure a daemon is reachable on `port` (probe the real port; spawn + wait only if nothing's there). */
208
269
  function ensureDaemon(port) {
209
- return probeDaemon(port).then((listening) => {
270
+ return probeDaemon(port).then(async (listening) => {
271
+ // Attaching to whatever already owns the port is the whole point of a daemon — but it means an
272
+ // upgrade does NOT take effect until that daemon dies, and nothing used to say so. Say it here,
273
+ // where both versions are in hand, and keep attaching: killing another agent's daemon on a
274
+ // version bump is worse than a loud warning.
210
275
  if (listening)
211
- return undefined;
276
+ return warnOnDaemonSkew(port);
212
277
  const scriptPath = process.argv[1];
213
278
  if (scriptPath === undefined)
214
279
  throw new Error('cannot locate the reticle daemon script');
@@ -222,6 +287,24 @@ function ensureDaemon(port) {
222
287
  * the daemon, then reuses the already-connected tab or opens a new browser at the url. Idempotent:
223
288
  * re-running never piles up duplicate tabs.
224
289
  */
290
+ /** How long `reticle open` waits for the launched page to register before reporting on it. */
291
+ const OPEN_SESSION_WAIT_MS = 8000;
292
+ /**
293
+ * Poll until a NEW session shows up, or the wait runs out. Returns whether one did.
294
+ *
295
+ * Compares against the count taken before launching rather than against zero, so opening a second
296
+ * app while one is already connected is not reported as an instant success.
297
+ */
298
+ async function waitForNewSession(port, before) {
299
+ const deadline = Date.now() + OPEN_SESSION_WAIT_MS;
300
+ while (Date.now() < deadline) {
301
+ await new Promise((resolve) => setTimeout(resolve, 400));
302
+ const { sessions } = summarizeStatus(await fetchStatus(port));
303
+ if (sessions.length > before)
304
+ return true;
305
+ }
306
+ return false;
307
+ }
225
308
  function handleOpen(requestedPort, url) {
226
309
  probeDaemon(requestedPort)
227
310
  .then((here) => (here ? requestedPort : (discoverDaemonPort() ?? requestedPort)))
@@ -229,7 +312,7 @@ function handleOpen(requestedPort, url) {
229
312
  await ensureDaemon(port);
230
313
  const { sessions } = summarizeStatus(await fetchStatus(port));
231
314
  const decision = decideOpen(sessions, url);
232
- if (decision.action === 'need-url') {
315
+ if ('need-url' === decision.action) {
233
316
  log('reticle_open', {
234
317
  port,
235
318
  error: 'no app connected — pass a url: reticle open <url>. Desktop app (Electron/Tauri)? ' +
@@ -237,12 +320,53 @@ function handleOpen(requestedPort, url) {
237
320
  });
238
321
  return;
239
322
  }
240
- if (decision.action === 'reuse') {
323
+ if ('reuse' === decision.action) {
241
324
  log('reticle_open', { port, reusing: decision.url });
242
325
  return;
243
326
  }
244
- openInBrowser(decision.url);
245
- log('reticle_open', { port, opened: decision.url });
327
+ // A tab on the right origin, on the WRONG page. Kept (that is what stops a tab piling up per
328
+ // run) but never reported as done: `reusing` here read as "your url is open" for a page nobody
329
+ // had opened.
330
+ if ('left-as-is' === decision.action) {
331
+ log('reticle_open', {
332
+ port,
333
+ reusing: decision.url,
334
+ requested: decision.requested,
335
+ note: `a tab is connected on this origin but sitting on ${decision.url} — it was LEFT THERE, ` +
336
+ `not navigated to ${decision.requested}. Drive it with reticle_navigate, or open the url ` +
337
+ 'in the browser yourself.',
338
+ });
339
+ return;
340
+ }
341
+ const launchError = await openInBrowser(decision.url);
342
+ if (launchError !== null) {
343
+ log('reticle_open', {
344
+ port,
345
+ error: `could not launch a browser: ${launchError}`,
346
+ recovery: 'Open the url yourself, or run `reticle doctor` — it checks the pieces this command ' +
347
+ 'depends on.',
348
+ });
349
+ process.exit(1);
350
+ return;
351
+ }
352
+ // Say whether a SESSION appeared, not merely that a launcher was invoked. `{"opened": url}`
353
+ // was printed unconditionally, so a run where nothing ever opened looked identical to a run
354
+ // where it did — reported from the field as twenty minutes lost to a phantom port problem.
355
+ const connected = await waitForNewSession(port, sessions.length);
356
+ log('reticle_open', {
357
+ port,
358
+ opened: decision.url,
359
+ connected,
360
+ ...(connected
361
+ ? {}
362
+ : {
363
+ note: 'the browser was launched but no Reticle session appeared. The app may still be ' +
364
+ 'loading, or it is not wired to this bridge — run `reticle doctor`, and check the ' +
365
+ 'app connects on port ' +
366
+ String(port) +
367
+ '.',
368
+ }),
369
+ });
246
370
  })
247
371
  .catch((err) => {
248
372
  log('reticle_open', { error: err instanceof Error ? err.message : String(err) });
@@ -284,12 +408,20 @@ function handleDaemonInner(parsed) {
284
408
  removePid(parsed.port);
285
409
  process.exit(1);
286
410
  });
287
- const shutdown = () => {
411
+ const shutdown = (reason) => {
412
+ // Recorded BEFORE the async chain: `installExitTrace` reads it when Node is on its way out,
413
+ // which is after everything below has run. Without it the exit line says `code: 0` and a
414
+ // reader cannot tell a tidy stop from the bridge disappearing — see #123.
415
+ recordExitReason(reason);
288
416
  // Awaited before the close/exit chain: `process.exit(0)` kills an in-flight POST, and this is
289
417
  // the one event carrying the whole session. A failed send resolves anyway (emit swallows its
290
418
  // own errors), so this can delay the exit by at most the send timeout, never prevent it.
291
419
  void daemonTelemetry
292
- .shutdown()
420
+ // The reason rides out on the session summary, which is the only event that fires at this
421
+ // exact moment. The proxy emits the matching `mcp_connection_lost` and cannot know it —
422
+ // it sees a socket end and nothing more, which is how a scheduled idle exit came to make up
423
+ // 299 of 321 "outages". See SessionSummary.exit.
424
+ .shutdown(reason)
293
425
  .then(() => server.close())
294
426
  .then(() => {
295
427
  removePid(parsed.port);
@@ -302,19 +434,43 @@ function handleDaemonInner(parsed) {
302
434
  process.exit(1);
303
435
  });
304
436
  };
305
- process.on('SIGTERM', shutdown);
306
- process.on('SIGINT', shutdown);
437
+ // Wrapped rather than passed directly: a signal handler receives the signal name as its first
438
+ // argument, which would otherwise arrive as the reason.
439
+ process.on('SIGTERM', () => shutdown(DaemonExitReason.SIGNAL));
440
+ process.on('SIGINT', () => shutdown(DaemonExitReason.SIGNAL));
307
441
  // Self-shut-down when idle so a detached daemon (and any headless Chromium it launched) never
308
442
  // lingers on the user's machine after the editor closes. Reuses the same clean shutdown path.
443
+ const attachedGraceEnv = process.env[ReticleEnv.IDLE_ATTACHED];
309
444
  const idleShutdown = new IdleShutdown({
310
445
  graceMs: resolveIdleShutdownMs(process.env[ReticleEnv.IDLE_SHUTDOWN]),
446
+ checkIntervalMs: resolveIdleCheckMs(process.env[ReticleEnv.IDLE_CHECK]),
311
447
  isIdle: server.isIdle ?? (() => false),
448
+ ...(server.agentAttached === undefined ? {} : { agentAttached: server.agentAttached }),
449
+ // Only when the var is actually SET. `resolveIdleShutdownMs(undefined)` returns the 5-minute
450
+ // DEFAULT, so passing it unconditionally would override the derived attached grace with the
451
+ // very number this change exists to stop using — shipping the bug while looking fixed.
452
+ ...(attachedGraceEnv === undefined || '' === attachedGraceEnv.trim()
453
+ ? {}
454
+ : { attachedGraceMs: resolveIdleShutdownMs(attachedGraceEnv) }),
312
455
  onShutdown: () => {
313
456
  log('reticle_daemon_idle_exit', { port: parsed.port });
314
- shutdown();
457
+ shutdown(DaemonExitReason.IDLE);
315
458
  },
316
459
  });
317
460
  idleShutdown.start();
461
+ // Say so, regularly, so a GAP in this log is itself evidence. `installExitTrace` hooks
462
+ // `'exit'`, which a SIGKILL never fires — so a killed daemon left nothing behind, and the one
463
+ // that exited tidily logged `code: 0`. A reader could not tell "shut down cleanly" from "the
464
+ // bridge every app on this machine needs is gone", and a correct SvelteKit install was written
465
+ // up as an install failure on exactly that ambiguity. See daemon/heartbeat.ts.
466
+ new DaemonHeartbeat({
467
+ log,
468
+ intervalMs: resolveHeartbeatMs(process.env[ReticleEnv.HEARTBEAT]),
469
+ facts: () => ({
470
+ sessions: server.bridge.sessions.count(),
471
+ served: everServedToolCall(),
472
+ }),
473
+ }).start();
318
474
  })
319
475
  .catch((error) => {
320
476
  const message = error instanceof Error ? error.message : String(error);
@@ -333,43 +489,85 @@ function handleDaemonInner(parsed) {
333
489
  */
334
490
  function handleMcp(opts) {
335
491
  const { port, driveUrl, headless, http, httpPort, httpToken } = opts;
336
- // Probe the port first — a daemon with a stale PID file is still usable.
337
- // Only spawn when nothing is actually listening on the port.
338
- probeDaemon(port)
339
- .then((listening) => {
340
- if (!listening) {
341
- const scriptPath = process.argv[1];
342
- if (scriptPath === undefined) {
343
- log('reticle_mcp_no_script', {});
344
- process.exit(1);
345
- return;
346
- }
347
- const daemonArgs = [DAEMON_INNER_COMMAND, PORT_FLAG, String(port)];
348
- if (driveUrl !== undefined) {
349
- daemonArgs.push(DRIVE_FLAG, driveUrl);
350
- if (!headless)
351
- daemonArgs.push(HEADED_FLAG);
352
- }
353
- // Forward the HTTP-verify flags too (previously silently dropped for `reticle mcp`).
354
- if (http) {
355
- daemonArgs.push(HTTP_FLAG);
356
- if (httpPort !== undefined)
357
- daemonArgs.push(HTTP_PORT_FLAG, String(httpPort));
358
- if (httpToken !== undefined)
359
- daemonArgs.push(HTTP_TOKEN_FLAG, httpToken);
360
- }
361
- spawnDaemon(process.execPath, scriptPath, daemonArgs, port);
362
- log('reticle_mcp_daemon_started', {
363
- port,
364
- ...(driveUrl !== undefined ? { driveUrl } : {}),
365
- });
492
+ // The proxy IS the MCP server the editor launched. Nothing respawns it, so an uncaught throw here
493
+ // is what a user experiences as "the MCP server disconnected — open /mcp and reconnect". Log it,
494
+ // report it, keep serving: the reconnect and dormant paths already know how to rebuild the only
495
+ // state this process has. See installProxyResilience for why its rule inverts the daemon's.
496
+ // The proxy's crash handlers must write to the proxy's LOG FILE, not just stderr. The editor
497
+ // swallows stderr, so wiring these to `log` meant a crash was handled and then unrecorded — the
498
+ // failure a user reports as "it disconnected" left nothing behind to read.
499
+ setProxyLogPort(port);
500
+ installProxyResilience(process, proxyLog);
501
+ /**
502
+ * Make sure a daemon is on the port, spawning one if not.
503
+ *
504
+ * Called on first start AND on every reconnect. The proxy used to only retry the socket, so a
505
+ * daemon that exited — crashed, `reticle stop`, or self-shut-down as idle — meant the retries hit
506
+ * a dead port until the budget ran out and the agent's MCP server exited with it. Reticle simply
507
+ * disappeared mid-session with nothing said. Respawning here makes the reconnect self-healing.
508
+ */
509
+ const ensure = async () => {
510
+ if (await probeDaemon(port))
511
+ return;
512
+ const scriptPath = process.argv[1];
513
+ if (scriptPath === undefined) {
514
+ log('reticle_mcp_no_script', {});
515
+ process.exit(1);
366
516
  }
367
- return waitForDaemon(port).then(() => startMcpProxy(port));
368
- })
517
+ const daemonArgs = [DAEMON_INNER_COMMAND, PORT_FLAG, String(port)];
518
+ if (driveUrl !== undefined) {
519
+ daemonArgs.push(DRIVE_FLAG, driveUrl);
520
+ if (!headless)
521
+ daemonArgs.push(HEADED_FLAG);
522
+ }
523
+ // Forward the HTTP-verify flags too (previously silently dropped for `reticle mcp`).
524
+ if (http) {
525
+ daemonArgs.push(HTTP_FLAG);
526
+ if (httpPort !== undefined)
527
+ daemonArgs.push(HTTP_PORT_FLAG, String(httpPort));
528
+ if (httpToken !== undefined)
529
+ daemonArgs.push(HTTP_TOKEN_FLAG, httpToken);
530
+ }
531
+ spawnDaemon(process.execPath, scriptPath, daemonArgs, port);
532
+ log('reticle_mcp_daemon_started', { port, ...(driveUrl !== undefined ? { driveUrl } : {}) });
533
+ await waitForDaemon(port);
534
+ };
535
+ // Start the proxy WHATEVER happened to the daemon.
536
+ //
537
+ // This used to exit(1) when `ensure` failed, which is the third way an MCP server disappears on a
538
+ // user: something else is holding the bridge port — a foreign daemon from another project, a
539
+ // half-dead process, a port a colleague's tool grabbed — the spawned daemon cannot bind, and the
540
+ // editor shows a server that failed to start. Nothing about that is unrecoverable: the proxy
541
+ // answers `initialize` itself, serves the cached catalog, and its wake path retries a daemon on
542
+ // every client request. Present-and-complaining beats absent, because absent needs a human.
543
+ // `void`: the chain handles its own failure and the process must not wait on it — the proxy is
544
+ // started from `finally` either way.
545
+ void ensure()
369
546
  .catch((err) => {
370
- const message = err instanceof Error ? err.message : String(err);
371
- log('reticle_mcp_proxy_error', { error: message });
372
- process.exit(1);
547
+ log('reticle_mcp_daemon_unavailable', {
548
+ error: err instanceof Error ? err.message : String(err),
549
+ note: 'serving anyway — the next tool call will try to start a daemon again',
550
+ });
551
+ })
552
+ .finally(() => {
553
+ // Also `void`: the proxy runs for the life of the process and is never awaited by anyone.
554
+ //
555
+ // The `.catch` is load-bearing despite that. `startMcpProxy` rejects when its FIRST connect
556
+ // fails — which is precisely the case this block exists to tolerate, a daemon that is not
557
+ // there yet — and with nothing attached that reject became an unhandledRejection. Reported
558
+ // from a win32 user as a crash reading `connect ECONNREFUSED` with an EMPTY frame list,
559
+ // because the stack of a refused socket is entirely node internals and the privacy filter
560
+ // keeps only Reticle frames. So the one path we most want diagnosable arrived as an anonymous
561
+ // crash. The proxy itself is unaffected — it has already installed its stdin reader and goes
562
+ // on serving from cache, waking a daemon on the next request — which is exactly why this
563
+ // must be logged as the expected condition it is rather than reported as a defect.
564
+ void startMcpProxy(port, ensure).catch((err) => {
565
+ log('reticle_mcp_proxy_first_connect_failed', {
566
+ port,
567
+ error: err instanceof Error ? err.message : String(err),
568
+ note: 'serving from cache; the next client request will try to start a daemon',
569
+ });
570
+ });
373
571
  });
374
572
  }
375
573
  function handleLegacyDrive(parsed) {
@@ -424,7 +622,11 @@ function main() {
424
622
  const parsed = parseCliArgs(argv, defaultPort, process.env['CI'] !== undefined);
425
623
  switch (parsed.kind) {
426
624
  case 'error':
625
+ // Two audiences, two channels. The structured line is for logs and scripts; the plain text is
626
+ // for the person who just typed the command. Emitting only the JSON meant a single typo came
627
+ // back as an escaped one-line wall of the entire help text — see the ParseError builders.
427
628
  log('reticle_usage_error', { message: parsed.message });
629
+ process.stderr.write(parsed.message === CLI_USAGE ? `${CLI_USAGE}\n` : `${parsed.message}\n\n${CLI_USAGE}\n`);
428
630
  process.exit(1);
429
631
  break;
430
632
  case 'init':
@@ -446,7 +648,7 @@ function main() {
446
648
  handleTelemetry(parsed.action);
447
649
  break;
448
650
  case 'feedback':
449
- void handleFeedback(parsed.text, parsed.rating, parsed.bug);
651
+ void handleFeedback(parsed.text, parsed.rating, parsed.bug, parsed.feedbackKind, parsed.agent);
450
652
  break;
451
653
  case 'identify':
452
654
  void handleIdentify(parsed);
@@ -13,7 +13,6 @@ export interface SyncPolicy {
13
13
  /** Sync saved flow files (the shared regression suite). */
14
14
  flows: boolean;
15
15
  }
16
- export declare const DEFAULT_SYNC_POLICY: SyncPolicy;
17
16
  /** Where a verification actually executes. Reserved for the hosted-runner path; default local. */
18
17
  export declare const VerifyMode: {
19
18
  readonly LOCAL: "local";
@@ -16,10 +16,10 @@ import { resolveCloudConfig } from './cloud-sync.js';
16
16
  export const CLOUD_LINK_FILE = 'cloud.json';
17
17
  /** `~/.reticle/credentials.json` — the user's per-cloud-project API keys (the only secret). */
18
18
  export const CREDENTIALS_FILE = 'credentials.json';
19
- export const DEFAULT_SYNC_POLICY = { runs: true, memory: true, flows: true };
19
+ const DEFAULT_SYNC_POLICY = { runs: true, memory: true, flows: true };
20
20
  /** Where a verification actually executes. Reserved for the hosted-runner path; default local. */
21
21
  export const VerifyMode = { LOCAL: 'local', SERVER: 'server' };
22
- const asBool = (v, fallback) => (typeof v === 'boolean' ? v : fallback);
22
+ const asBool = (v, fallback) => ('boolean' === typeof v ? v : fallback);
23
23
  /** Read + JSON-parse a file, or null on any problem (missing/malformed) — never throws. */
24
24
  async function readJson(fs, path) {
25
25
  try {
@@ -33,14 +33,14 @@ async function readJson(fs, path) {
33
33
  }
34
34
  /** Validate the shape of `.reticle/cloud.json`. Requires projectId + url; policy/verify default in. */
35
35
  function parseLink(raw) {
36
- if (typeof raw !== 'object' || raw === null)
36
+ if (typeof raw !== 'object' || null === raw)
37
37
  return null;
38
38
  const o = raw;
39
- if (typeof o.projectId !== 'string' || o.projectId.length === 0)
39
+ if (typeof o.projectId !== 'string' || 0 === o.projectId.length)
40
40
  return null;
41
- if (typeof o.url !== 'string' || o.url.length === 0)
41
+ if (typeof o.url !== 'string' || 0 === o.url.length)
42
42
  return null;
43
- const sync = typeof o.sync === 'object' && o.sync !== null ? o.sync : {};
43
+ const sync = 'object' === typeof o.sync && o.sync !== null ? o.sync : {};
44
44
  return {
45
45
  projectId: o.projectId,
46
46
  url: o.url,
@@ -54,10 +54,10 @@ function parseLink(raw) {
54
54
  }
55
55
  /** Look up the API key for a cloud project id in the user credentials map. */
56
56
  function credentialFor(raw, projectId) {
57
- if (typeof raw !== 'object' || raw === null)
57
+ if (typeof raw !== 'object' || null === raw)
58
58
  return null;
59
59
  const key = raw[projectId];
60
- return typeof key === 'string' && key.length > 0 ? key : null;
60
+ return 'string' === typeof key && key.length > 0 ? key : null;
61
61
  }
62
62
  /**
63
63
  * Resolve the cloud picture for a project rooted at `reticleRoot`. Reads the project's link file + the
@@ -66,7 +66,7 @@ function credentialFor(raw, projectId) {
66
66
  */
67
67
  export async function resolveProjectCloud(fs, reticleRoot, homeDir, env) {
68
68
  const link = parseLink(await readJson(fs, join(reticleRoot, CLOUD_LINK_FILE)));
69
- if (link === null) {
69
+ if (null === link) {
70
70
  // No per-project link → the env vars are the whole story (legacy single-project / CI behaviour).
71
71
  return {
72
72
  config: resolveCloudConfig(env),
@@ -15,12 +15,6 @@ export declare const CloudEnv: {
15
15
  readonly URL: "RETICLE_CLOUD_URL";
16
16
  readonly KEY: "RETICLE_CLOUD_KEY";
17
17
  };
18
- /** Paths the OSS server pushes to (match the cloud app's contract). */
19
- export declare const CLOUD_FLOWS_PATH = "/v1/flows";
20
- export declare const CLOUD_RUNS_PATH = "/v1/runs";
21
- export declare const CLOUD_PROJECT_RUNS_PATH = "/v1/project/runs";
22
- export declare const CLOUD_PROJECT_REGRESSION_PATH = "/v1/project/regression";
23
- export declare const CLOUD_VERIFICATIONS_PATH = "/v1/verifications";
24
18
  export interface CloudConfig {
25
19
  url: string;
26
20
  apiKey: string;
@@ -33,7 +27,7 @@ export declare const SyncOutcome: {
33
27
  readonly FAILED: "failed";
34
28
  };
35
29
  export type SyncOutcome = (typeof SyncOutcome)[keyof typeof SyncOutcome];
36
- export interface SyncResult {
30
+ interface SyncResult {
37
31
  outcome: SyncOutcome;
38
32
  status?: number;
39
33
  error?: string;