@reticlehq/server 3.8.2 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (477) hide show
  1. package/LICENSE +201 -104
  2. package/NOTICE +5 -0
  3. package/README.md +1 -1
  4. package/SKILL.md +13 -12
  5. package/bin/launch.js +13 -0
  6. package/bin/package.json +3 -0
  7. package/bin/reticle.js +66 -8
  8. package/dist/chromium-launch-options.d.ts +25 -0
  9. package/dist/chromium-launch-options.js +21 -1
  10. package/dist/command/cli/adhoc-suite.js +2 -0
  11. package/dist/command/cli/adhoc-verdict.d.ts +2 -43
  12. package/dist/command/cli/adhoc-verdict.js +50 -160
  13. package/dist/command/cli/answers/gate-exit.d.ts +6 -2
  14. package/dist/command/cli/answers/gate-exit.js +12 -2
  15. package/dist/command/cli/answers/mcp-connection-hint.d.ts +2 -0
  16. package/dist/command/cli/answers/mcp-connection-hint.js +6 -0
  17. package/dist/command/cli/ci-hint.d.ts +44 -0
  18. package/dist/command/cli/ci-hint.js +72 -0
  19. package/dist/command/cli/cli-doctor.d.ts +6 -1
  20. package/dist/command/cli/cli-doctor.js +46 -5
  21. package/dist/command/cli/cli-flow-commands.d.ts +7 -1
  22. package/dist/command/cli/cli-flow-commands.js +39 -24
  23. package/dist/command/cli/cli-kill.d.ts +21 -0
  24. package/dist/command/cli/cli-kill.js +47 -4
  25. package/dist/command/cli/cli-parse-compare.d.ts +22 -0
  26. package/dist/command/cli/cli-parse-compare.js +75 -0
  27. package/dist/command/cli/cli-parse-grade.d.ts +36 -0
  28. package/dist/command/cli/cli-parse-grade.js +75 -0
  29. package/dist/command/cli/cli-parse-grammar.d.ts +2 -0
  30. package/dist/command/cli/cli-parse-grammar.js +18 -20
  31. package/dist/command/cli/cli-parse-verify.d.ts +10 -0
  32. package/dist/command/cli/cli-parse-verify.js +63 -14
  33. package/dist/command/cli/cli-parse.d.ts +22 -17
  34. package/dist/command/cli/cli-parse.js +54 -32
  35. package/dist/command/cli/cli-update-commands.d.ts +1 -1
  36. package/dist/command/cli/cli-update-commands.js +29 -20
  37. package/dist/command/cli/cli-usage.js +61 -31
  38. package/dist/command/cli/cli-verify.d.ts +17 -3
  39. package/dist/command/cli/cli-verify.js +59 -21
  40. package/dist/command/cli/cloud-cli.js +2 -119
  41. package/dist/command/cli/daemon-link/daemon-client.d.ts +45 -0
  42. package/dist/command/cli/daemon-link/daemon-client.js +205 -0
  43. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +6 -1
  44. package/dist/command/cli/doctor/browser/chromium-hint.js +11 -5
  45. package/dist/command/cli/doctor/doctor-daemon-line.js +6 -3
  46. package/dist/command/cli/doctor/doctor-fix.d.ts +44 -0
  47. package/dist/command/cli/doctor/doctor-fix.js +89 -0
  48. package/dist/command/cli/doctor/doctor-launcher-line.d.ts +23 -0
  49. package/dist/command/cli/doctor/doctor-launcher-line.js +60 -0
  50. package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
  51. package/dist/command/cli/doctor/doctor-rows.js +4 -0
  52. package/dist/command/cli/doctor-repairs.d.ts +64 -0
  53. package/dist/command/cli/doctor-repairs.js +214 -0
  54. package/dist/command/cli/drive/drive-command.js +3 -7
  55. package/dist/command/cli/ensure-daemon.d.ts +26 -0
  56. package/dist/command/cli/ensure-daemon.js +57 -0
  57. package/dist/command/cli/finished-run/grade-command.d.ts +23 -0
  58. package/dist/command/cli/finished-run/grade-command.js +180 -0
  59. package/dist/command/cli/first-use-checks.d.ts +41 -0
  60. package/dist/command/cli/first-use-checks.js +127 -0
  61. package/dist/command/cli/first-verdict-node.d.ts +3 -0
  62. package/dist/command/cli/first-verdict-node.js +189 -0
  63. package/dist/command/cli/first-verdict.d.ts +126 -0
  64. package/dist/command/cli/first-verdict.js +267 -0
  65. package/dist/command/cli/launch/sdk-version-match.d.ts +3 -2
  66. package/dist/command/cli/launch/sdk-version-match.js +3 -2
  67. package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +1 -1
  68. package/dist/command/cli/lifecycle/daemon-lifecycle.js +7 -6
  69. package/dist/command/cli/mcp-command.js +13 -4
  70. package/dist/command/cli/ports/port-holder.d.ts +11 -0
  71. package/dist/command/cli/ports/port-holder.js +36 -4
  72. package/dist/command/cli/setup-mcp-cli.d.ts +27 -1
  73. package/dist/command/cli/setup-mcp-cli.js +98 -23
  74. package/dist/command/cli/status/status-lines.d.ts +2 -0
  75. package/dist/command/cli/status/status-lines.js +72 -17
  76. package/dist/command/cli/status-fields.d.ts +5 -1
  77. package/dist/command/cli/status-fields.js +24 -6
  78. package/dist/command/cli/try-command.d.ts +7 -0
  79. package/dist/command/cli/try-command.js +11 -3
  80. package/dist/command/cli/two-builds/compare-command.d.ts +48 -0
  81. package/dist/command/cli/two-builds/compare-command.js +314 -0
  82. package/dist/command/cli/verify-against.d.ts +61 -0
  83. package/dist/command/cli/verify-against.js +187 -0
  84. package/dist/command/cli/verify-no-session.d.ts +2 -0
  85. package/dist/command/cli/verify-no-session.js +13 -0
  86. package/dist/command/cli/verify-results.d.ts +35 -2
  87. package/dist/command/cli/verify-results.js +76 -10
  88. package/dist/command/cli.js +112 -70
  89. package/dist/command/daemon/binding/daemon-status-probe.d.ts +3 -1
  90. package/dist/command/daemon/binding/daemon-status-probe.js +4 -2
  91. package/dist/command/daemon/daemon-argv.d.ts +9 -0
  92. package/dist/command/daemon/daemon-argv.js +11 -0
  93. package/dist/command/daemon/daemon-resolve.d.ts +8 -1
  94. package/dist/command/daemon/daemon-resolve.js +27 -1
  95. package/dist/command/daemon/daemon.d.ts +22 -4
  96. package/dist/command/daemon/daemon.js +52 -10
  97. package/dist/command/daemon/lifetime/daemon-exit.d.ts +24 -0
  98. package/dist/command/daemon/lifetime/daemon-exit.js +39 -0
  99. package/dist/command/daemon/lifetime/death-cause.d.ts +15 -0
  100. package/dist/command/daemon/lifetime/death-cause.js +149 -0
  101. package/dist/command/daemon/lifetime/heartbeat.d.ts +7 -15
  102. package/dist/command/daemon/lifetime/heartbeat.js +11 -15
  103. package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
  104. package/dist/command/daemon/lifetime/startup-failure.js +62 -0
  105. package/dist/command/daemon/port-state.d.ts +14 -0
  106. package/dist/command/daemon/port-state.js +31 -0
  107. package/dist/command/setup/agent-configs.d.ts +2 -0
  108. package/dist/command/setup/agent-configs.js +42 -15
  109. package/dist/command/setup/agent-next-steps.d.ts +77 -0
  110. package/dist/command/setup/agent-next-steps.js +284 -0
  111. package/dist/command/setup/agent-writer.js +11 -6
  112. package/dist/command/setup/bringup/dev-server-files.d.ts +9 -0
  113. package/dist/command/setup/bringup/dev-server-files.js +17 -0
  114. package/dist/command/setup/bringup/static-page-server.d.ts +14 -2
  115. package/dist/command/setup/bringup/static-page-server.js +43 -9
  116. package/dist/command/setup/init/init-host.js +2 -0
  117. package/dist/command/setup/init/init-runtime.d.ts +6 -1
  118. package/dist/command/setup/init/init-runtime.js +63 -21
  119. package/dist/command/setup/mcp-launcher.d.ts +3 -0
  120. package/dist/command/setup/mcp-launcher.js +11 -0
  121. package/dist/command/setup/node-effects.d.ts +3 -2
  122. package/dist/command/setup/node-effects.js +9 -3
  123. package/dist/command/setup/remaining-steps.d.ts +8 -0
  124. package/dist/command/setup/remaining-steps.js +10 -0
  125. package/dist/command/setup/run-setup.d.ts +2 -0
  126. package/dist/command/setup/run-setup.js +9 -1
  127. package/dist/command/setup/setup-command.js +36 -15
  128. package/dist/command/setup/setup-mcp.d.ts +8 -0
  129. package/dist/command/setup/setup-mcp.js +47 -20
  130. package/dist/command/setup/start-dev-server.d.ts +8 -0
  131. package/dist/command/setup/start-dev-server.js +43 -0
  132. package/dist/command/update/updater.js +6 -0
  133. package/dist/command/version/identity/server-version.d.ts +2 -0
  134. package/dist/command/version/identity/server-version.js +2 -0
  135. package/dist/command/version/version-skew.d.ts +7 -0
  136. package/dist/command/version/version-skew.js +14 -3
  137. package/dist/features/compare/action-plan.d.ts +260 -0
  138. package/dist/features/compare/action-plan.js +543 -0
  139. package/dist/features/compare/behaviour-compare.d.ts +24 -0
  140. package/dist/features/compare/behaviour-compare.js +85 -0
  141. package/dist/features/compare/behaviour-diff.d.ts +97 -0
  142. package/dist/features/compare/behaviour-diff.js +202 -0
  143. package/dist/features/compare/behaviour-report.d.ts +40 -0
  144. package/dist/features/compare/behaviour-report.js +128 -0
  145. package/dist/features/compare/build-readings.d.ts +106 -0
  146. package/dist/features/compare/build-readings.js +250 -0
  147. package/dist/features/compare/compare-diff.d.ts +62 -0
  148. package/dist/features/compare/compare-diff.js +177 -0
  149. package/dist/features/compare/compare-report-html.d.ts +6 -0
  150. package/dist/features/compare/compare-report-html.js +102 -0
  151. package/dist/features/compare/compare-report.d.ts +15 -0
  152. package/dist/features/compare/compare-report.js +73 -0
  153. package/dist/features/compare/compare-run.d.ts +60 -0
  154. package/dist/features/compare/compare-run.js +123 -0
  155. package/dist/features/compare/outside/effect-lines.d.ts +26 -0
  156. package/dist/features/compare/outside/effect-lines.js +103 -0
  157. package/dist/features/compare/outside/state-sources.d.ts +76 -0
  158. package/dist/features/compare/outside/state-sources.js +164 -0
  159. package/dist/features/compare/outside/value-dictionary.d.ts +51 -0
  160. package/dist/features/compare/outside/value-dictionary.js +227 -0
  161. package/dist/features/compare/visible-content.d.ts +20 -0
  162. package/dist/features/compare/visible-content.js +125 -0
  163. package/dist/features/crawl/crawl-empty.js +2 -1
  164. package/dist/features/crawl/crawl-exhaustive.d.ts +55 -0
  165. package/dist/features/crawl/crawl-exhaustive.js +138 -0
  166. package/dist/features/crawl/crawl-tools.d.ts +9 -2
  167. package/dist/features/crawl/crawl-tools.js +28 -12
  168. package/dist/features/crawl/crawl.d.ts +8 -9
  169. package/dist/features/crawl/crawl.js +61 -65
  170. package/dist/features/exhaust/app-coverage.d.ts +5 -1
  171. package/dist/features/exhaust/app-coverage.js +3 -1
  172. package/dist/features/exhaust/code-coverage.js +8 -4
  173. package/dist/features/exhaust/edge-cases.d.ts +133 -0
  174. package/dist/features/exhaust/edge-cases.js +310 -0
  175. package/dist/features/exhaust/edges/edge-drive.d.ts +42 -0
  176. package/dist/features/exhaust/edges/edge-drive.js +364 -0
  177. package/dist/features/exhaust/edges/edge-run.d.ts +59 -0
  178. package/dist/features/exhaust/edges/edge-run.js +169 -0
  179. package/dist/features/exhaust/explorer.d.ts +87 -14
  180. package/dist/features/exhaust/explorer.js +159 -25
  181. package/dist/features/exhaust/failure-branches.d.ts +5 -0
  182. package/dist/features/exhaust/failure-branches.js +29 -11
  183. package/dist/features/exhaust/invariants.d.ts +112 -0
  184. package/dist/features/exhaust/invariants.js +188 -0
  185. package/dist/features/exhaust/ledger.d.ts +68 -0
  186. package/dist/features/exhaust/ledger.js +121 -13
  187. package/dist/features/exhaust/session-fold.d.ts +10 -0
  188. package/dist/features/exhaust/session-fold.js +46 -3
  189. package/dist/features/exhaust/session-port.d.ts +3 -1
  190. package/dist/features/exhaust/session-port.js +97 -28
  191. package/dist/features/grade/grade-answer.d.ts +99 -0
  192. package/dist/features/grade/grade-answer.js +112 -0
  193. package/dist/features/grade/grade-noise.d.ts +27 -0
  194. package/dist/features/grade/grade-noise.js +55 -0
  195. package/dist/features/grade/grade-renumber.d.ts +12 -0
  196. package/dist/features/grade/grade-renumber.js +86 -0
  197. package/dist/features/grade/grade-state.d.ts +50 -0
  198. package/dist/features/grade/grade-state.js +93 -0
  199. package/dist/features/grade/grade-suggest.d.ts +33 -0
  200. package/dist/features/grade/grade-suggest.js +98 -0
  201. package/dist/features/license/license.d.ts +11 -71
  202. package/dist/features/license/license.js +43 -136
  203. package/dist/features/visual/desktop-capture.d.ts +33 -0
  204. package/dist/features/visual/desktop-capture.js +94 -0
  205. package/dist/features/visual/step-shots.js +139 -35
  206. package/dist/features/visual/visual-tools.d.ts +0 -18
  207. package/dist/features/visual/visual-tools.js +67 -99
  208. package/dist/import-graph.js +5 -6
  209. package/dist/index.d.ts +3 -1
  210. package/dist/index.js +19 -7
  211. package/dist/judgement/capsule/causal-summary.js +9 -2
  212. package/dist/judgement/runs/artifact/replay-mapping.d.ts +13 -1
  213. package/dist/judgement/runs/artifact/replay-mapping.js +1 -1
  214. package/dist/judgement/runs/reticle-runner.js +16 -2
  215. package/dist/judgement/runs/run-ledger.d.ts +7 -5
  216. package/dist/judgement/runs/run-ledger.js +98 -69
  217. package/dist/judgement/runs/runner-port.d.ts +2 -0
  218. package/dist/judgement/runs/runner-port.js +23 -5
  219. package/dist/judgement/runs/verification-sync.d.ts +6 -3
  220. package/dist/judgement/runs/verification-sync.js +21 -10
  221. package/dist/language/flows/affected-tools.d.ts +1 -1
  222. package/dist/language/flows/affected-tools.js +49 -30
  223. package/dist/language/flows/change/git-changed.d.ts +2 -1
  224. package/dist/language/flows/change/git-changed.js +19 -2
  225. package/dist/language/flows/fields/flow-secret-field.d.ts +1 -13
  226. package/dist/language/flows/fields/flow-secret-field.js +77 -21
  227. package/dist/language/flows/flow-anchor.d.ts +60 -2
  228. package/dist/language/flows/flow-anchor.js +125 -20
  229. package/dist/language/flows/flow-classify.d.ts +20 -0
  230. package/dist/language/flows/flow-classify.js +90 -12
  231. package/dist/language/flows/flow-cross-step.d.ts +7 -1
  232. package/dist/language/flows/flow-cross-step.js +25 -1
  233. package/dist/language/flows/flow-journey.d.ts +38 -4
  234. package/dist/language/flows/flow-journey.js +138 -8
  235. package/dist/language/flows/flow-replay-run.d.ts +0 -16
  236. package/dist/language/flows/flow-replay-run.js +76 -14
  237. package/dist/language/flows/flow-replay-types.d.ts +4 -2
  238. package/dist/language/flows/flow-replay.d.ts +19 -1
  239. package/dist/language/flows/flow-replay.js +169 -52
  240. package/dist/language/flows/flow-replayable.js +7 -1
  241. package/dist/language/flows/flow-start-path.d.ts +34 -5
  242. package/dist/language/flows/flow-start-path.js +202 -15
  243. package/dist/language/flows/flow-step-runners.d.ts +14 -13
  244. package/dist/language/flows/flow-step-runners.js +184 -109
  245. package/dist/language/flows/flow-store-for-session.d.ts +26 -25
  246. package/dist/language/flows/flow-store-for-session.js +51 -5
  247. package/dist/language/flows/flow-tools.js +46 -33
  248. package/dist/language/flows/flows.js +176 -76
  249. package/dist/language/flows/recording/tape/recordings.d.ts +21 -16
  250. package/dist/language/flows/recording/tape/recordings.js +112 -49
  251. package/dist/language/flows/replay.d.ts +36 -12
  252. package/dist/language/flows/replay.js +193 -50
  253. package/dist/language/flows/steps/page-steps.d.ts +68 -0
  254. package/dist/language/flows/steps/page-steps.js +198 -0
  255. package/dist/language/flows/suite/sequential-run.d.ts +68 -0
  256. package/dist/language/flows/suite/sequential-run.js +96 -0
  257. package/dist/language/flows/verify-change-tools.js +26 -15
  258. package/dist/launch-chromium.d.ts +1 -0
  259. package/dist/launch-chromium.js +49 -12
  260. package/dist/memory/cloud/background-sync.d.ts +13 -0
  261. package/dist/memory/cloud/background-sync.js +24 -3
  262. package/dist/memory/cloud/sync-daemon.js +26 -27
  263. package/dist/memory/cloud/sync-screenshots.js +6 -1
  264. package/dist/memory/journal/attach-journal.js +2 -1
  265. package/dist/memory/journal/drive-flow.d.ts +7 -0
  266. package/dist/memory/journal/drive-flow.js +44 -13
  267. package/dist/memory/journal/journal-recorder.d.ts +4 -2
  268. package/dist/memory/journal/session-journal.d.ts +7 -0
  269. package/dist/memory/journal/session-journal.js +58 -10
  270. package/dist/memory/project/root-claims.d.ts +9 -0
  271. package/dist/memory/project/root-claims.js +9 -4
  272. package/dist/portal/bridge/bridge.d.ts +13 -4
  273. package/dist/portal/bridge/bridge.js +10 -9
  274. package/dist/portal/input/download-capture.d.ts +7 -0
  275. package/dist/portal/input/download-capture.js +92 -0
  276. package/dist/portal/input/network-detail.d.ts +6 -0
  277. package/dist/portal/input/network-detail.js +12 -1
  278. package/dist/portal/input/network-mock-tools.js +49 -13
  279. package/dist/portal/input/network-mock.d.ts +8 -0
  280. package/dist/portal/input/network-mock.js +30 -10
  281. package/dist/portal/input/page-capture.d.ts +24 -0
  282. package/dist/portal/input/page-capture.js +33 -0
  283. package/dist/portal/input/real-input.d.ts +35 -18
  284. package/dist/portal/input/real-input.js +99 -33
  285. package/dist/portal/input/viewport-tools.d.ts +8 -0
  286. package/dist/portal/input/viewport-tools.js +81 -21
  287. package/dist/portal/pool/browser-pool.d.ts +16 -30
  288. package/dist/portal/pool/browser-pool.js +64 -52
  289. package/dist/portal/pool/context-permissions.d.ts +34 -17
  290. package/dist/portal/pool/context-permissions.js +94 -1
  291. package/dist/portal/pool/csp-widen.d.ts +62 -0
  292. package/dist/portal/pool/csp-widen.js +132 -0
  293. package/dist/portal/pool/determinism.d.ts +52 -0
  294. package/dist/portal/pool/determinism.js +259 -0
  295. package/dist/portal/pool/lease-emulation.d.ts +34 -1
  296. package/dist/portal/pool/lease-emulation.js +18 -0
  297. package/dist/portal/pool/lease-marker-stamp.d.ts +6 -0
  298. package/dist/portal/pool/lease-marker-stamp.js +15 -1
  299. package/dist/portal/pool/lease-page-error.d.ts +56 -0
  300. package/dist/portal/pool/lease-page-error.js +160 -0
  301. package/dist/portal/pool/page-error-journal.d.ts +63 -0
  302. package/dist/portal/pool/page-error-journal.js +208 -0
  303. package/dist/portal/pool/playwright-launcher.js +71 -15
  304. package/dist/portal/pool/pool-contract.d.ts +49 -1
  305. package/dist/portal/pool/pool-lease.d.ts +70 -2
  306. package/dist/portal/pool/pool-lease.js +23 -0
  307. package/dist/portal/pool/zero-install.d.ts +8 -0
  308. package/dist/portal/pool/zero-install.js +91 -1
  309. package/dist/portal/session/dev-server/dev-command.d.ts +11 -2
  310. package/dist/portal/session/dev-server/dev-command.js +110 -16
  311. package/dist/portal/session/dev-server/dev-server-probe.d.ts +7 -0
  312. package/dist/portal/session/dev-server/dev-server-probe.js +9 -0
  313. package/dist/portal/session/dev-server/project-ports.d.ts +21 -0
  314. package/dist/portal/session/dev-server/project-ports.js +62 -0
  315. package/dist/portal/session/facts/departed-network.d.ts +66 -0
  316. package/dist/portal/session/facts/departed-network.js +142 -0
  317. package/dist/portal/session/no-session-diagnosis.d.ts +9 -0
  318. package/dist/portal/session/no-session-diagnosis.js +76 -27
  319. package/dist/portal/session/no-session-next-action.d.ts +14 -0
  320. package/dist/portal/session/no-session-next-action.js +56 -7
  321. package/dist/portal/session/no-session-watch.d.ts +6 -0
  322. package/dist/portal/session/no-session-watch.js +41 -4
  323. package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
  324. package/dist/portal/session/page-commands/undeclared-command.js +16 -7
  325. package/dist/portal/session/recorded-gaps.d.ts +1 -1
  326. package/dist/portal/session/recorded-gaps.js +5 -2
  327. package/dist/portal/session/serving-directory.d.ts +10 -0
  328. package/dist/portal/session/serving-directory.js +39 -8
  329. package/dist/portal/session/session-manager.d.ts +10 -0
  330. package/dist/portal/session/session-manager.js +211 -81
  331. package/dist/portal/session/session.js +4 -3
  332. package/dist/portal/session/zero-install-first-call.d.ts +20 -1
  333. package/dist/portal/session/zero-install-first-call.js +34 -2
  334. package/dist/start-wiring.d.ts +30 -2
  335. package/dist/start-wiring.js +74 -9
  336. package/dist/surface/mcp/mcp-proxy.js +10 -2
  337. package/dist/surface/mcp/mcp.js +8 -5
  338. package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
  339. package/dist/surface/mcp/peer/agents-connected.js +58 -0
  340. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
  341. package/dist/surface/mcp/shared-params.d.ts +2 -0
  342. package/dist/surface/mcp/shared-params.js +31 -0
  343. package/dist/surface/tools/act/act-observation.d.ts +40 -2
  344. package/dist/surface/tools/act/act-observation.js +118 -12
  345. package/dist/surface/tools/act/act-preflight.js +38 -18
  346. package/dist/surface/tools/act/act-sequence-retry.d.ts +23 -2
  347. package/dist/surface/tools/act/act-sequence-retry.js +61 -5
  348. package/dist/surface/tools/act/act-target.js +3 -1
  349. package/dist/surface/tools/act/after-match.d.ts +10 -0
  350. package/dist/surface/tools/act/after-match.js +40 -0
  351. package/dist/surface/tools/act/already-true.js +1 -1
  352. package/dist/surface/tools/act/navigation/reload-result.d.ts +12 -1
  353. package/dist/surface/tools/act/navigation/reload-result.js +25 -2
  354. package/dist/surface/tools/act/resolve-target.d.ts +17 -1
  355. package/dist/surface/tools/act/resolve-target.js +59 -9
  356. package/dist/surface/tools/act/sequence-grade.d.ts +15 -1
  357. package/dist/surface/tools/act/sequence-grade.js +45 -27
  358. package/dist/surface/tools/act/settle-in-flight.js +27 -15
  359. package/dist/surface/tools/act-command.d.ts +23 -0
  360. package/dist/surface/tools/act-command.js +50 -0
  361. package/dist/surface/tools/act-sequence-tool.js +81 -50
  362. package/dist/surface/tools/act-tools.d.ts +1 -13
  363. package/dist/surface/tools/act-tools.js +83 -95
  364. package/dist/surface/tools/agent/build-staleness.d.ts +1 -1
  365. package/dist/surface/tools/agent/build-staleness.js +1 -1
  366. package/dist/surface/tools/assert/assert-verdict.js +18 -12
  367. package/dist/surface/tools/browser-tools.js +87 -13
  368. package/dist/surface/tools/compare-tools.d.ts +11 -0
  369. package/dist/surface/tools/compare-tools.js +94 -0
  370. package/dist/surface/tools/coverage-tools.js +1 -1
  371. package/dist/surface/tools/dynamic-tools.js +11 -3
  372. package/dist/surface/tools/empty-session-list.d.ts +2 -0
  373. package/dist/surface/tools/empty-session-list.js +4 -2
  374. package/dist/surface/tools/error-recovery.d.ts +12 -1
  375. package/dist/surface/tools/error-recovery.js +131 -40
  376. package/dist/surface/tools/explore-tools.js +1 -1
  377. package/dist/surface/tools/invoke-tool.js +29 -21
  378. package/dist/surface/tools/lease-coverage-tools.d.ts +49 -0
  379. package/dist/surface/tools/lease-coverage-tools.js +128 -0
  380. package/dist/surface/tools/lease-hint.d.ts +42 -1
  381. package/dist/surface/tools/lease-hint.js +52 -3
  382. package/dist/surface/tools/lease-readiness.d.ts +47 -0
  383. package/dist/surface/tools/lease-readiness.js +126 -0
  384. package/dist/surface/tools/lease-seed.d.ts +16 -1
  385. package/dist/surface/tools/lease-seed.js +37 -4
  386. package/dist/surface/tools/lease-tools.d.ts +3 -41
  387. package/dist/surface/tools/lease-tools.js +104 -124
  388. package/dist/surface/tools/lease-url-identity.d.ts +46 -0
  389. package/dist/surface/tools/lease-url-identity.js +97 -0
  390. package/dist/surface/tools/merge-tools.js +99 -2
  391. package/dist/surface/tools/merged-name-redirect.js +5 -1
  392. package/dist/surface/tools/observe-tools.js +15 -9
  393. package/dist/surface/tools/read/console-pre-attach.d.ts +19 -0
  394. package/dist/surface/tools/read/console-pre-attach.js +23 -0
  395. package/dist/surface/tools/read-tools.js +33 -30
  396. package/dist/surface/tools/real-input-attempt.d.ts +0 -8
  397. package/dist/surface/tools/real-input-attempt.js +143 -28
  398. package/dist/surface/tools/repeat-hint.d.ts +40 -0
  399. package/dist/surface/tools/repeat-hint.js +171 -0
  400. package/dist/surface/tools/run-report.d.ts +2 -2
  401. package/dist/surface/tools/run-report.js +27 -15
  402. package/dist/surface/tools/session-ports.d.ts +14 -0
  403. package/dist/surface/tools/session-ports.js +53 -0
  404. package/dist/surface/tools/tool-kit.d.ts +18 -0
  405. package/dist/surface/tools/tool-kit.js +9 -3
  406. package/dist/surface/tools/tool-surface.js +0 -1
  407. package/dist/surface/tools/tools.js +54 -18
  408. package/dist/surface/tools/type-spelling.d.ts +13 -0
  409. package/dist/surface/tools/type-spelling.js +36 -0
  410. package/dist/telemetry/agent-client.d.ts +6 -0
  411. package/dist/telemetry/agent-client.js +10 -0
  412. package/dist/telemetry/argument-shape.js +17 -7
  413. package/dist/telemetry/cli-telemetry.d.ts +10 -1
  414. package/dist/telemetry/cli-telemetry.js +15 -0
  415. package/dist/telemetry/init-telemetry.d.ts +2 -1
  416. package/dist/telemetry/init-telemetry.js +47 -2
  417. package/dist/telemetry/install-trace.js +23 -4
  418. package/dist/telemetry/onboarding-funnel.d.ts +19 -2
  419. package/dist/telemetry/onboarding-funnel.js +25 -3
  420. package/dist/telemetry/proxy-telemetry.d.ts +2 -1
  421. package/dist/telemetry/proxy-telemetry.js +2 -2
  422. package/dist/telemetry/session-metrics.d.ts +1 -1
  423. package/dist/telemetry/session-metrics.js +67 -27
  424. package/dist/telemetry/telemetry.d.ts +3 -1
  425. package/dist/telemetry/telemetry.js +60 -28
  426. package/dist/telemetry/tool-refused.d.ts +6 -3
  427. package/dist/telemetry/tool-refused.js +15 -3
  428. package/docs/actions.mdx +3 -3
  429. package/docs/agent-cheatsheet.md +1 -1
  430. package/docs/architecture.md +3 -6
  431. package/docs/capabilities.mdx +61 -1
  432. package/docs/cli/affected.mdx +9 -7
  433. package/docs/cli/cloud.mdx +3 -3
  434. package/docs/cli/compare.mdx +94 -0
  435. package/docs/cli/doctor.mdx +41 -2
  436. package/docs/cli/grade.mdx +97 -0
  437. package/docs/cli/help.mdx +4 -2
  438. package/docs/cli/license.mdx +13 -12
  439. package/docs/cli/restart.mdx +7 -5
  440. package/docs/cli/rollback.mdx +5 -5
  441. package/docs/cli/setup.mdx +13 -2
  442. package/docs/cli/status.mdx +4 -0
  443. package/docs/cli/stop.mdx +3 -3
  444. package/docs/cli/update.mdx +2 -2
  445. package/docs/cli/verify.mdx +23 -1
  446. package/docs/cli.mdx +34 -24
  447. package/docs/deploy-checks.md +25 -0
  448. package/docs/desktop.mdx +1 -1
  449. package/docs/enterprise.md +17 -22
  450. package/docs/environment.mdx +1 -0
  451. package/docs/first-drive.md +1 -1
  452. package/docs/flows.md +11 -1
  453. package/docs/frameworks.mdx +9 -9
  454. package/docs/getting-started.md +1 -1
  455. package/docs/install-manual.mdx +16 -1
  456. package/docs/license-activation.mdx +3 -3
  457. package/docs/packages/engine.mdx +1 -1
  458. package/docs/packages/server.mdx +1 -1
  459. package/docs/packages/tauri.mdx +1 -1
  460. package/docs/packages/test.mdx +1 -1
  461. package/docs/packages.mdx +1 -1
  462. package/docs/platform-integration.md +1 -1
  463. package/docs/predicates.mdx +1 -1
  464. package/docs/quickstart.mdx +68 -49
  465. package/docs/skill-setup.md +1 -1
  466. package/docs/skill.md +14 -13
  467. package/docs/telemetry.md +6 -4
  468. package/docs/tools/navigate.mdx +1 -1
  469. package/docs/tools/verify.mdx +4 -1
  470. package/docs/troubleshooting.mdx +1 -1
  471. package/docs/usage.md +17 -14
  472. package/package.json +18 -14
  473. package/LICENSE-ENTERPRISE +0 -37
  474. package/dist/features/ee/audit-log.d.ts +0 -14
  475. package/dist/features/ee/audit-log.js +0 -11
  476. package/dist/features/exhaust/exhaust-run.d.ts +0 -33
  477. package/dist/features/exhaust/exhaust-run.js +0 -82
@@ -1,10 +1,11 @@
1
1
  import { join } from 'node:path';
2
- import { homedir } from 'node:os';
2
+ import { homedir, uptime } from 'node:os';
3
3
  import { mkdirSync, readFileSync, writeFileSync, existsSync, unlinkSync, openSync, closeSync, readdirSync, statSync, renameSync, writeSync, } from 'node:fs';
4
4
  import { spawn } from 'node:child_process';
5
5
  import { daemonRegistryFileName, DaemonSpawnMethod, ReticleEnv, } from '@reticlehq/core';
6
6
  import { log } from '../../log.js';
7
7
  import { uncleanPredecessor } from './lifetime/unclean-predecessor.js';
8
+ import { deathCauseFromLog, readLogTail } from './lifetime/death-cause.js';
8
9
  import { reportDaemonDiedUnclean } from '../../telemetry/proxy-telemetry.js';
9
10
  import { MAX_DAEMON_LOG_BYTES, pruneOldDaemonLogs } from './lifetime/log-cap.js';
10
11
  import { escapeJobViaWmi } from './win32-escape.js';
@@ -59,13 +60,18 @@ export function readPid(port, home = reticleStateHome()) {
59
60
  const n = parseInt(readFileSync(path, 'utf8').trim(), 10);
60
61
  return isNaN(n) ? null : n;
61
62
  }
63
+ /**
64
+ * Whether a process with this pid exists. `EPERM` means it exists and this process may not signal
65
+ * it, which a daemon that WMI started can answer on Windows; only `ESRCH` means it is gone. Reading
66
+ * every throw as "dead" reclaimed a live daemon and started a second one on its port.
67
+ */
62
68
  export function isAlive(pid) {
63
69
  try {
64
70
  process.kill(pid, 0);
65
71
  return true;
66
72
  }
67
- catch {
68
- return false;
73
+ catch (err) {
74
+ return err instanceof Error && 'code' in err && 'EPERM' === err.code;
69
75
  }
70
76
  }
71
77
  export function writePid(port) {
@@ -149,13 +155,42 @@ export function discoverDaemonPort() {
149
155
  }
150
156
  return found;
151
157
  }
158
+ /**
159
+ * When the machine last booted, in epoch ms. A pidfile written before it names no daemon: every
160
+ * process from before the boot is gone, and its pid may now belong to anything.
161
+ */
162
+ function lastBootMs() {
163
+ // The clock is read at this I/O boundary, as the other timestamps in this file are.
164
+ return Date.now() - uptime() * 1000;
165
+ }
166
+ /**
167
+ * Does this pidfile name a daemon that is still running?
168
+ *
169
+ * The pid must be alive AND the file must be newer than the last boot. A pid alone is not enough:
170
+ * the OS reuses pids, and after a reboot a pidfile named an unrelated Bluetooth service on Windows.
171
+ * The port was free, but serve skipped the start because that pid was alive, and the daemon never
172
+ * came up. A file whose age cannot be read is judged by its pid alone, as before.
173
+ */
174
+ export function pidFileIsLive(path, pid, pidAlive, bootMs = lastBootMs) {
175
+ if (!pidAlive(pid)) {
176
+ return false;
177
+ }
178
+ let writtenMs;
179
+ try {
180
+ writtenMs = statSync(path).mtimeMs;
181
+ }
182
+ catch {
183
+ return true;
184
+ }
185
+ return writtenMs >= bootMs();
186
+ }
152
187
  /**
153
188
  * Sweep ~/.reticle for daemon-<port>.pid files whose process is no longer alive and delete them, so a
154
189
  * crashed daemon never leaves a stale pidfile that confuses discovery or makes a port look "taken".
155
190
  * Returns the ports reclaimed. `home` and `pidAlive` are injectable for testing (default to the real
156
191
  * ~/.reticle and the process.kill(pid,0) liveness probe).
157
192
  */
158
- export function reclaimStaleDaemons(home = reticleStateHome(), pidAlive = isAlive) {
193
+ export function reclaimStaleDaemons(home = reticleStateHome(), pidAlive = isAlive, bootMs = lastBootMs) {
159
194
  const reclaimed = [];
160
195
  let files;
161
196
  try {
@@ -178,7 +213,7 @@ export function reclaimStaleDaemons(home = reticleStateHome(), pidAlive = isAliv
178
213
  catch {
179
214
  pid = null; // unreadable pidfile counts as stale
180
215
  }
181
- if (null === pid || !pidAlive(pid)) {
216
+ if (null === pid || !pidFileIsLive(path, pid, pidAlive, bootMs)) {
182
217
  try {
183
218
  unlinkSync(path);
184
219
  removeDaemonRegistry(Number(match[1])); // drop the sidecar discovery entry too
@@ -206,17 +241,22 @@ function defaultSpawnDaemonDeps() {
206
241
  pidAlive: isAlive,
207
242
  platform: process.platform,
208
243
  escapeJob: escapeJobViaWmi,
209
- onUncleanDeath: (ageMs) => void reportDaemonDiedUnclean(ageMs),
244
+ onUncleanDeath: (ageMs, cause) => void reportDaemonDiedUnclean(ageMs, cause),
210
245
  };
211
246
  }
212
247
  /**
213
248
  * Report a dead predecessor, aged from its pidfile: the parent writes that file at spawn time.
214
249
  * Read before the unlink, because after it nothing is left to age. A report must never stop a start.
215
250
  */
216
- function reportUncleanAge(pidFilePath, deps) {
251
+ function reportUncleanAge(pidFilePath, logFilePath, deps) {
217
252
  try {
218
253
  // The clock is read at this I/O boundary, the same as the log timestamps in this file.
219
- deps.onUncleanDeath?.(Math.max(0, Date.now() - statSync(pidFilePath).mtimeMs));
254
+ const now = Date.now();
255
+ const startedMs = statSync(pidFilePath).mtimeMs;
256
+ // Read before `rotateDaemonLog`, which renames the file this daemon wrote.
257
+ const tail = readLogTail(logFilePath);
258
+ const cause = tail === undefined ? undefined : deathCauseFromLog(tail, startedMs, now);
259
+ deps.onUncleanDeath?.(Math.max(0, now - startedMs), cause);
220
260
  }
221
261
  catch {
222
262
  // An unreadable pidfile has no age to report.
@@ -276,14 +316,16 @@ export function spawnDaemon(nodeExec, scriptPath, args, port, deps = defaultSpaw
276
316
  }
277
317
  catch {
278
318
  const existing = readPid(port, deps.home);
279
- if (existing !== null && deps.pidAlive(existing))
319
+ if (existing !== null &&
320
+ pidFileIsLive(pidFilePath, existing, (pid) => deps.pidAlive(pid), deps.bootTimeMs)) {
280
321
  return false;
322
+ }
281
323
  // This branch IS the crash detection — its own comment has always said so ("a stale pidfile from
282
324
  // a crashed daemon is reclaimed"), it just never said it out loud. Captured before the unlink,
283
325
  // because after it there is no trace left that the previous daemon died at all.
284
326
  orphanPid = uncleanPredecessor(existing, false, process.pid);
285
327
  if (null !== orphanPid) {
286
- reportUncleanAge(pidFilePath, deps);
328
+ reportUncleanAge(pidFilePath, logFilePath, deps);
287
329
  }
288
330
  try {
289
331
  unlinkSync(pidFilePath);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The daemon's exit, with a bound (#1937).
3
+ *
4
+ * The exit used to await the telemetry summary and then `server.close()`, with no limit on the
5
+ * close. The close waits for the browser pool, the cloud sync flush and every open HTTP connection.
6
+ * When one of them never settled, the process stayed alive with its port held, and every tool call
7
+ * found a daemon that did not answer.
8
+ */
9
+ export declare const ExitCode: {
10
+ readonly CLEAN: 0;
11
+ readonly FAILED: 1;
12
+ };
13
+ export type ExitCode = (typeof ExitCode)[keyof typeof ExitCode];
14
+ /**
15
+ * The most the summary and the close get together. The summary send has its own 2s bound, and the
16
+ * pool needs a few seconds to close its browsers.
17
+ */
18
+ export declare const DAEMON_EXIT_BUDGET_MS = 10000;
19
+ /** Send the summary, close, and resolve to the exit code. Never takes longer than `budgetMs`. */
20
+ export declare function exitAfterClose(steps: {
21
+ summary: () => Promise<void>;
22
+ close: () => Promise<void>;
23
+ budgetMs?: number;
24
+ }): Promise<ExitCode>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The daemon's exit, with a bound (#1937).
3
+ *
4
+ * The exit used to await the telemetry summary and then `server.close()`, with no limit on the
5
+ * close. The close waits for the browser pool, the cloud sync flush and every open HTTP connection.
6
+ * When one of them never settled, the process stayed alive with its port held, and every tool call
7
+ * found a daemon that did not answer.
8
+ */
9
+ import { log } from '../../../log.js';
10
+ export const ExitCode = {
11
+ CLEAN: 0,
12
+ FAILED: 1,
13
+ };
14
+ /**
15
+ * The most the summary and the close get together. The summary send has its own 2s bound, and the
16
+ * pool needs a few seconds to close its browsers.
17
+ */
18
+ export const DAEMON_EXIT_BUDGET_MS = 10_000;
19
+ /** Send the summary, close, and resolve to the exit code. Never takes longer than `budgetMs`. */
20
+ export async function exitAfterClose(steps) {
21
+ const budgetMs = steps.budgetMs ?? DAEMON_EXIT_BUDGET_MS;
22
+ let timer;
23
+ const deadline = new Promise((_resolve, reject) => {
24
+ timer = setTimeout(() => {
25
+ reject(new Error(`the daemon did not finish its shutdown within ${String(budgetMs)}ms`));
26
+ }, budgetMs);
27
+ });
28
+ try {
29
+ await Promise.race([steps.summary().then(steps.close), deadline]);
30
+ return ExitCode.CLEAN;
31
+ }
32
+ catch (err) {
33
+ log('reticle_daemon_close_error', { error: err instanceof Error ? err.message : String(err) });
34
+ return ExitCode.FAILED;
35
+ }
36
+ finally {
37
+ clearTimeout(timer);
38
+ }
39
+ }
@@ -0,0 +1,15 @@
1
+ import { DaemonEnd, type DaemonDeath } from '@reticlehq/core';
2
+ /** What the log says about the dead daemon. `ageMs` is the caller's to add. */
3
+ export type DeathCause = Omit<DaemonDeath, 'ageMs'> & {
4
+ end: DaemonEnd;
5
+ };
6
+ /**
7
+ * Read the cause of death of the daemon that started at `startedMs`.
8
+ *
9
+ * The log is append-only across launches, so only lines at or after `startedMs` count. A daemon
10
+ * that crashed before it wrote any line leaves only a raw Node stack, after the last line of the
11
+ * daemon before it; the error code is read from there.
12
+ */
13
+ export declare function deathCauseFromLog(text: string, startedMs: number, nowMs: number): DeathCause;
14
+ /** The last part of the log at `path`, or undefined when it cannot be read. Never throws. */
15
+ export declare function readLogTail(path: string): string | undefined;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Why a daemon died, read from its own log by the start that finds it dead.
3
+ *
4
+ * The daemon log already says how a daemon ended: a heartbeat every 30 s, a line for a signal, a
5
+ * line for an exit, and a line for a failed start (see heartbeat.ts). Nobody uploads that file, so
6
+ * `daemon_died_unclean` carried only the age, and a reboot, a killed process tree, an out-of-memory
7
+ * kill and a crash at startup sent the same event. The next start reads the log before it rotates
8
+ * it, and sends the cause with the event.
9
+ *
10
+ * Only facts that are names or numbers leave this module: an end, a signal name, a Node error code,
11
+ * counts. Never a message, because a message can carry the user's paths.
12
+ */
13
+ import { closeSync, fstatSync, openSync, readSync } from 'node:fs';
14
+ import { DaemonEnd, DaemonStartupError, NODE_ERROR_CODE } from '@reticlehq/core';
15
+ import { DAEMON_HEARTBEAT_EVENT, classifyDaemonLife } from './heartbeat.js';
16
+ import { DAEMON_START_FAILED_EVENT } from './startup-failure.js';
17
+ /**
18
+ * A Node error code where Node writes one: `[ERR_MODULE_NOT_FOUND]`, `code: 'EADDRINUSE'`, or
19
+ * `listen EADDRINUSE:`. A bare upper-case word elsewhere is not a code.
20
+ */
21
+ const ERROR_CODE_IN_TEXT = /\b(E[A-Z]{3,30}|ERR_[A-Z0-9_]{2,60})(?=\]|'|"|:)/g;
22
+ /** Upper-case words that sit before a colon and are not codes. */
23
+ const NOT_A_CODE = new Set(['ERROR', 'EXIT', 'ENV']);
24
+ function parse(raw) {
25
+ let record;
26
+ try {
27
+ record = JSON.parse(raw);
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ if ('object' !== typeof record || null === record) {
33
+ return undefined;
34
+ }
35
+ const r = record;
36
+ const t = 'string' === typeof r['t'] ? Date.parse(r['t']) : Number.NaN;
37
+ if (!Number.isFinite(t) || 'string' !== typeof r['event']) {
38
+ return undefined;
39
+ }
40
+ return { ...r, t, event: r['event'] };
41
+ }
42
+ /** The last Node error code in `text`, or undefined. Only the code, never the text around it. */
43
+ function lastErrorCode(text) {
44
+ const codes = [...text.matchAll(ERROR_CODE_IN_TEXT)].map((m) => m[1]);
45
+ const code = codes.filter((c) => c !== undefined && !NOT_A_CODE.has(c)).at(-1);
46
+ return code !== undefined && NODE_ERROR_CODE.test(code) ? code : undefined;
47
+ }
48
+ /** The stage of a failed start, only when it is a member of the closed list. */
49
+ function startupStage(v) {
50
+ return Object.values(DaemonStartupError).find((stage) => stage === v);
51
+ }
52
+ /** The code field of a failed start, only when it has the shape of a Node error code. */
53
+ function codeField(v) {
54
+ return 'string' === typeof v && NODE_ERROR_CODE.test(v) ? v : undefined;
55
+ }
56
+ const count = (v) => 'number' === typeof v && Number.isInteger(v) && 0 <= v ? v : undefined;
57
+ /**
58
+ * Read the cause of death of the daemon that started at `startedMs`.
59
+ *
60
+ * The log is append-only across launches, so only lines at or after `startedMs` count. A daemon
61
+ * that crashed before it wrote any line leaves only a raw Node stack, after the last line of the
62
+ * daemon before it; the error code is read from there.
63
+ */
64
+ export function deathCauseFromLog(text, startedMs, nowMs) {
65
+ const ours = [];
66
+ const rawAfterLastLine = [];
67
+ const rawInOurs = [];
68
+ for (const raw of text.split('\n')) {
69
+ const trimmed = raw.trim();
70
+ if (0 === trimmed.length) {
71
+ continue;
72
+ }
73
+ const parsed = parse(trimmed);
74
+ if (parsed === undefined) {
75
+ (0 < ours.length ? rawInOurs : rawAfterLastLine).push(trimmed);
76
+ continue;
77
+ }
78
+ if (parsed.t >= startedMs) {
79
+ ours.push(parsed);
80
+ }
81
+ else {
82
+ rawAfterLastLine.length = 0;
83
+ }
84
+ }
85
+ const life = classifyDaemonLife(ours, nowMs);
86
+ const cause = { end: life.end };
87
+ if (life.silentForMs !== undefined) {
88
+ cause.silentMs = Math.max(0, Math.round(life.silentForMs));
89
+ }
90
+ if (life.signal !== undefined && /^SIG[A-Z0-9]{1,12}$/.test(life.signal)) {
91
+ cause.signal = life.signal;
92
+ }
93
+ const lastBeat = ours.findLast((l) => l.event === DAEMON_HEARTBEAT_EVENT);
94
+ if (lastBeat !== undefined) {
95
+ const sessions = count(lastBeat['sessions']);
96
+ const rssMb = count(lastBeat['rssMb']);
97
+ if (sessions !== undefined) {
98
+ cause.sessions = sessions;
99
+ }
100
+ if ('boolean' === typeof lastBeat['served']) {
101
+ cause.served = lastBeat['served'];
102
+ }
103
+ if (rssMb !== undefined) {
104
+ cause.rssMb = rssMb;
105
+ }
106
+ }
107
+ const failed = ours.findLast((l) => l.event === DAEMON_START_FAILED_EVENT);
108
+ const stage = startupStage(failed?.['stage']);
109
+ if (stage !== undefined) {
110
+ cause.startupError = stage;
111
+ }
112
+ const error = codeField(failed?.['code']) ??
113
+ ('string' === typeof failed?.['error'] ? lastErrorCode(failed['error']) : undefined) ??
114
+ // Raw lines before this daemon's first line belong to the daemon before it, unless this one
115
+ // wrote no line at all, which is a crash before its first log line.
116
+ lastErrorCode((0 < ours.length ? rawInOurs : rawAfterLastLine).join('\n'));
117
+ if (error !== undefined) {
118
+ cause.error = error;
119
+ }
120
+ // No line of its own, and a Node error after the last line of the daemon before it: Node stopped
121
+ // this daemon before its code could write anything, for example on a module that did not load.
122
+ if (0 === ours.length && error !== undefined) {
123
+ cause.end = DaemonEnd.START_FAILED;
124
+ cause.startupError = DaemonStartupError.BEFORE_LOG;
125
+ }
126
+ return cause;
127
+ }
128
+ /** How much of the end of a daemon log the next start reads. The last beats and a stack fit. */
129
+ const LOG_TAIL_BYTES = 256 * 1024;
130
+ /** The last part of the log at `path`, or undefined when it cannot be read. Never throws. */
131
+ export function readLogTail(path) {
132
+ let fd;
133
+ try {
134
+ fd = openSync(path, 'r');
135
+ const size = fstatSync(fd).size;
136
+ const length = Math.min(size, LOG_TAIL_BYTES);
137
+ const buffer = Buffer.alloc(length);
138
+ readSync(fd, buffer, 0, length, size - length);
139
+ return buffer.toString('utf8');
140
+ }
141
+ catch {
142
+ return undefined;
143
+ }
144
+ finally {
145
+ if (fd !== undefined) {
146
+ closeSync(fd);
147
+ }
148
+ }
149
+ }
@@ -17,6 +17,7 @@
17
17
  *
18
18
  * A heartbeat nobody interprets is just log volume.
19
19
  */
20
+ import { DaemonEnd } from '@reticlehq/core';
20
21
  /** The event name a beat carries. One `grep` separates liveness from everything else in the file. */
21
22
  export declare const DAEMON_HEARTBEAT_EVENT = "reticle_daemon_alive";
22
23
  /**
@@ -43,6 +44,11 @@ interface HeartbeatFacts {
43
44
  * and make a long-lived daemon look freshly idle.
44
45
  */
45
46
  served: boolean;
47
+ /**
48
+ * Resident memory in MB. Optional, so a caller that cannot read it sends no number rather than
49
+ * a guess. The next start reports it when this daemon dies, which separates a memory death.
50
+ */
51
+ rssMb?: number;
46
52
  }
47
53
  interface DaemonHeartbeatOptions {
48
54
  log: (event: string, fields: Record<string, unknown>) => void;
@@ -74,20 +80,7 @@ export declare class DaemonHeartbeat {
74
80
  start(): void;
75
81
  stop(): void;
76
82
  }
77
- /** How a daemon's life ended, as far as its own log can say. */
78
- export declare const DaemonEnd: {
79
- /** Still beating within tolerance. */
80
- readonly ALIVE: "alive";
81
- /** Logged its own exit, with no signal before it. */
82
- readonly CLEAN: "clean";
83
- /** A signal arrived — the cause an exit line with `code: 0` does not carry. */
84
- readonly SIGNALLED: "signalled";
85
- /** The beat simply stopped. No exit, no signal: killed, crashed, or swept. */
86
- readonly DIED_SILENTLY: "died_silently";
87
- /** No beat was ever emitted, so this log cannot answer the question. */
88
- readonly UNKNOWN: "unknown";
89
- };
90
- export type DaemonEnd = (typeof DaemonEnd)[keyof typeof DaemonEnd];
83
+ export { DaemonEnd };
91
84
  /**
92
85
  * One parsed line of a daemon log. The index signature is not laziness: these lines carry whatever
93
86
  * their event needed (`code`, `port`, `pid`, `reason`, …), and a reader that refused unknown keys
@@ -116,4 +109,3 @@ export interface DaemonLife {
116
109
  * signal we never sent is the same confident-wrong this module replaces.
117
110
  */
118
111
  export declare function classifyDaemonLife(lines: readonly LogLine[], nowMs: number): DaemonLife;
119
- export {};
@@ -17,7 +17,9 @@
17
17
  *
18
18
  * A heartbeat nobody interprets is just log volume.
19
19
  */
20
+ import { DaemonEnd } from '@reticlehq/core';
20
21
  import { DAEMON_LOG_TRUNCATED_EVENT, guardDaemonLog } from './log-cap.js';
22
+ import { DAEMON_START_FAILED_EVENT } from './startup-failure.js';
21
23
  /** The event name a beat carries. One `grep` separates liveness from everything else in the file. */
22
24
  export const DAEMON_HEARTBEAT_EVENT = 'reticle_daemon_alive';
23
25
  /** Written by installExitTrace when the process exits through Node. */
@@ -90,6 +92,7 @@ export class DaemonHeartbeat {
90
92
  everyMs: this.#intervalMs,
91
93
  sessions: facts.sessions,
92
94
  served: facts.served,
95
+ ...(facts.rssMb === undefined ? {} : { rssMb: facts.rssMb }),
93
96
  });
94
97
  }
95
98
  start() {
@@ -106,19 +109,8 @@ export class DaemonHeartbeat {
106
109
  this.#timer = undefined;
107
110
  }
108
111
  }
109
- /** How a daemon's life ended, as far as its own log can say. */
110
- export const DaemonEnd = {
111
- /** Still beating within tolerance. */
112
- ALIVE: 'alive',
113
- /** Logged its own exit, with no signal before it. */
114
- CLEAN: 'clean',
115
- /** A signal arrived — the cause an exit line with `code: 0` does not carry. */
116
- SIGNALLED: 'signalled',
117
- /** The beat simply stopped. No exit, no signal: killed, crashed, or swept. */
118
- DIED_SILENTLY: 'died_silently',
119
- /** No beat was ever emitted, so this log cannot answer the question. */
120
- UNKNOWN: 'unknown',
121
- };
112
+ // The vocabulary lives in core, because `daemon_died_unclean` reports it too.
113
+ export { DaemonEnd };
122
114
  /**
123
115
  * Read a daemon log back and say how it ended.
124
116
  *
@@ -141,8 +133,12 @@ export function classifyDaemonLife(lines, nowMs) {
141
133
  };
142
134
  }
143
135
  const last = beats.at(-1);
144
- if (last === undefined)
145
- return { end: DaemonEnd.UNKNOWN };
136
+ if (last === undefined) {
137
+ // A daemon that logged a failed start never beat. Without this branch its death was `unknown`,
138
+ // which said "no evidence" about a log that held the cause (#2043).
139
+ const failed = lines.some((l) => l.event === DAEMON_START_FAILED_EVENT);
140
+ return { end: failed ? DaemonEnd.START_FAILED : DaemonEnd.UNKNOWN };
141
+ }
146
142
  const everyMs = 'number' === typeof last.everyMs ? last.everyMs : DAEMON_HEARTBEAT_MS;
147
143
  const silentForMs = nowMs - last.t;
148
144
  return silentForMs > everyMs * MISSED_BEATS_TOLERATED
@@ -1,5 +1,33 @@
1
+ import { DaemonStartupError } from '@reticlehq/core';
1
2
  /** The structured log event emitted when the daemon child cannot start. */
2
3
  export declare const DAEMON_START_FAILED_EVENT = "reticle_daemon_start_failed";
4
+ type LogFn = (event: string, fields: Record<string, unknown>) => void;
5
+ /**
6
+ * Write a failed start to the daemon log, before the process exits (#2043).
7
+ *
8
+ * The line has three fields. `error` is the message, for the person who reads the log and for
9
+ * `readDaemonStartupCause`. `code` is the Node error code, read from the error and not from the
10
+ * message. `stage` is where the start failed: a failed bind is always `listen`, whatever the caller
11
+ * gave. The next start reads `code` and `stage` into `daemon_died_unclean`. The message stays here.
12
+ */
13
+ export declare function recordStartupFailure(log: LogFn, error: unknown, stage: DaemonStartupError): void;
14
+ /** The parts of `process` that the startup trap uses. */
15
+ export interface StartupTrapTarget {
16
+ prependListener(event: 'uncaughtException' | 'unhandledRejection', handler: (value: unknown) => void): void;
17
+ off(event: 'uncaughtException' | 'unhandledRejection', handler: (value: unknown) => void): void;
18
+ }
19
+ /**
20
+ * Write a throw or a rejection that stops the start to the daemon log.
21
+ *
22
+ * Before the daemon is ready, the process crash guard owns these two events. It writes one sentence
23
+ * to stderr and exits, and that sentence has no event name and no code. So a daemon that died in
24
+ * its first 100 ms left a log that no reader could classify. This trap runs FIRST (it is prepended)
25
+ * and writes one structured line. It does not exit: the crash guard still does that.
26
+ *
27
+ * Returns a disposer. Call it when the daemon is ready, because from then on the daemon's own
28
+ * resilience handlers own these events, and a crash is no longer a failed start.
29
+ */
30
+ export declare function installStartupTrap(proc: StartupTrapTarget, log: LogFn): () => void;
3
31
  /**
4
32
  * Return the most recent startup cause written by this launch attempt.
5
33
  *
@@ -9,3 +37,4 @@ export declare const DAEMON_START_FAILED_EVENT = "reticle_daemon_start_failed";
9
37
  * unavailable logs are best-effort misses; the caller still has its generic fallback diagnosis.
10
38
  */
11
39
  export declare function readDaemonStartupCause(path: string, sinceMs: number): string | undefined;
40
+ export {};
@@ -1,6 +1,68 @@
1
1
  import { readFileSync } from 'node:fs';
2
+ import { DaemonStartupError, NODE_ERROR_CODE } from '@reticlehq/core';
2
3
  /** The structured log event emitted when the daemon child cannot start. */
3
4
  export const DAEMON_START_FAILED_EVENT = 'reticle_daemon_start_failed';
5
+ /** The `syscall` that Node puts on the error of a failed bind. */
6
+ const LISTEN_SYSCALL = 'listen';
7
+ /** The Node error code on `error`, or undefined. Only a value that has the shape of a code. */
8
+ function nodeErrorCode(error) {
9
+ if ('object' !== typeof error || null === error) {
10
+ return undefined;
11
+ }
12
+ const code = error.code;
13
+ return 'string' === typeof code && NODE_ERROR_CODE.test(code) ? code : undefined;
14
+ }
15
+ function isListenError(error) {
16
+ if ('object' !== typeof error || null === error) {
17
+ return false;
18
+ }
19
+ return LISTEN_SYSCALL === error.syscall;
20
+ }
21
+ /**
22
+ * Write a failed start to the daemon log, before the process exits (#2043).
23
+ *
24
+ * The line has three fields. `error` is the message, for the person who reads the log and for
25
+ * `readDaemonStartupCause`. `code` is the Node error code, read from the error and not from the
26
+ * message. `stage` is where the start failed: a failed bind is always `listen`, whatever the caller
27
+ * gave. The next start reads `code` and `stage` into `daemon_died_unclean`. The message stays here.
28
+ */
29
+ export function recordStartupFailure(log, error, stage) {
30
+ const code = nodeErrorCode(error);
31
+ log(DAEMON_START_FAILED_EVENT, {
32
+ stage: isListenError(error) ? DaemonStartupError.LISTEN : stage,
33
+ ...(code === undefined ? {} : { code }),
34
+ error: error instanceof Error ? error.message : String(error),
35
+ });
36
+ }
37
+ /**
38
+ * Write a throw or a rejection that stops the start to the daemon log.
39
+ *
40
+ * Before the daemon is ready, the process crash guard owns these two events. It writes one sentence
41
+ * to stderr and exits, and that sentence has no event name and no code. So a daemon that died in
42
+ * its first 100 ms left a log that no reader could classify. This trap runs FIRST (it is prepended)
43
+ * and writes one structured line. It does not exit: the crash guard still does that.
44
+ *
45
+ * Returns a disposer. Call it when the daemon is ready, because from then on the daemon's own
46
+ * resilience handlers own these events, and a crash is no longer a failed start.
47
+ */
48
+ export function installStartupTrap(proc, log) {
49
+ let written = false;
50
+ const trap = (stage) => (value) => {
51
+ if (written) {
52
+ return;
53
+ }
54
+ written = true;
55
+ recordStartupFailure(log, value, stage);
56
+ };
57
+ const onException = trap(DaemonStartupError.UNCAUGHT_EXCEPTION);
58
+ const onRejection = trap(DaemonStartupError.UNHANDLED_REJECTION);
59
+ proc.prependListener('uncaughtException', onException);
60
+ proc.prependListener('unhandledRejection', onRejection);
61
+ return () => {
62
+ proc.off('uncaughtException', onException);
63
+ proc.off('unhandledRejection', onRejection);
64
+ };
65
+ }
4
66
  /**
5
67
  * Return the most recent startup cause written by this launch attempt.
6
68
  *
@@ -0,0 +1,14 @@
1
+ /**
2
+ * What holds the bridge port, said the same way by every surface (#1937).
3
+ *
4
+ * A Reticle daemon stuck in its exit held the port and answered nothing. `status` called the port
5
+ * free, the MCP proxy called the holder "not a Reticle daemon", and `restart` refused to stop it.
6
+ * The pid file did not name it, because the next start had replaced that file. Its command line
7
+ * still did.
8
+ */
9
+ import { type PortHolder } from '../cli/ports/port-holder.js';
10
+ import { PortPresence } from './binding/port-presence.js';
11
+ /** The listener on `port`, and whether its command line is a Reticle daemon's. */
12
+ export declare function identifyPortHolder(port: number, exec: (command: string, args: readonly string[]) => string | null): PortHolder | null;
13
+ /** The sentence for the state of `port`, with the holder named when something holds it. */
14
+ export declare function describePortState(presence: PortPresence, port: number): string;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * What holds the bridge port, said the same way by every surface (#1937).
3
+ *
4
+ * A Reticle daemon stuck in its exit held the port and answered nothing. `status` called the port
5
+ * free, the MCP proxy called the holder "not a Reticle daemon", and `restart` refused to stop it.
6
+ * The pid file did not name it, because the next start had replaced that file. Its command line
7
+ * still did.
8
+ */
9
+ import { captureLookup, describeForeignHolder, findPortHolder, } from '../cli/ports/port-holder.js';
10
+ import { describePresence, PortPresence } from './binding/port-presence.js';
11
+ import { isReticleDaemonCommand } from './daemon-argv.js';
12
+ import { readPid } from './daemon.js';
13
+ /** The listener on `port`, and whether its command line is a Reticle daemon's. */
14
+ export function identifyPortHolder(port, exec) {
15
+ const holder = findPortHolder(port, exec);
16
+ if (null === holder) {
17
+ return null;
18
+ }
19
+ const commandLine = exec('ps', ['-o', 'command=', '-p', String(holder.pid)]);
20
+ return {
21
+ ...holder,
22
+ reticleDaemon: null !== commandLine && isReticleDaemonCommand(commandLine),
23
+ };
24
+ }
25
+ /** The sentence for the state of `port`, with the holder named when something holds it. */
26
+ export function describePortState(presence, port) {
27
+ if (PortPresence.FOREIGN !== presence) {
28
+ return describePresence(presence, port);
29
+ }
30
+ return describeForeignHolder(port, identifyPortHolder(port, captureLookup), readPid(port));
31
+ }
@@ -82,6 +82,8 @@ interface AgentPlanInput {
82
82
  readonly platform: keyof PlatformPaths;
83
83
  readonly exists: (path: string) => boolean;
84
84
  readonly readFile: (path: string) => string;
85
+ /** The installed bin to register (`InitHost.mcpLauncher`). Absent: the pinned npx command. */
86
+ readonly launcher?: string | undefined;
85
87
  }
86
88
  /**
87
89
  * Join for the platform the CALLER named, never the one this process happens to run on.