@reticlehq/server 3.1.0 → 3.3.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 (384) hide show
  1. package/SKILL.md +30 -28
  2. package/dist/command/cli/adhoc-suite.d.ts +19 -0
  3. package/dist/command/cli/adhoc-suite.js +88 -0
  4. package/dist/command/cli/adhoc-verdict.d.ts +23 -2
  5. package/dist/command/cli/adhoc-verdict.js +18 -10
  6. package/dist/command/cli/auth/device-flow.d.ts +2 -2
  7. package/dist/command/cli/cli-doctor.js +37 -5
  8. package/dist/command/cli/cli-flow-commands.d.ts +2 -1
  9. package/dist/command/cli/cli-kill.js +1 -1
  10. package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
  11. package/dist/command/cli/cli-parse-grammar.js +7 -0
  12. package/dist/command/cli/cli-parse-verify.d.ts +2 -5
  13. package/dist/command/cli/cli-parse-verify.js +86 -13
  14. package/dist/command/cli/cli-parse.d.ts +8 -1
  15. package/dist/command/cli/cli-parse.js +38 -7
  16. package/dist/command/cli/cli-verify.d.ts +59 -0
  17. package/dist/command/cli/cli-verify.js +131 -19
  18. package/dist/command/cli/cloud-cli.js +7 -6
  19. package/dist/command/cli/cloud-kit.d.ts +0 -2
  20. package/dist/command/cli/cloud-kit.js +3 -7
  21. package/dist/command/cli/config/config-discovery.d.ts +12 -0
  22. package/dist/command/cli/config/config-discovery.js +19 -0
  23. package/dist/command/cli/doctor/doctor-rows.d.ts +12 -0
  24. package/dist/command/cli/doctor/doctor-rows.js +12 -0
  25. package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
  26. package/dist/command/cli/drive/drive-command.js +1 -1
  27. package/dist/command/cli/launch/cli-launch.d.ts +3 -23
  28. package/dist/command/cli/launch/cli-launch.js +29 -70
  29. package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +33 -0
  30. package/dist/command/cli/lifecycle/daemon-lifecycle.js +198 -0
  31. package/dist/command/cli/mcp-command.js +5 -3
  32. package/dist/command/cli/ports/resolve/cli-port.d.ts +15 -1
  33. package/dist/command/cli/ports/resolve/cli-port.js +50 -2
  34. package/dist/command/cli/report-command.d.ts +16 -0
  35. package/dist/command/cli/report-command.js +97 -0
  36. package/dist/command/cli/status/status-lines.d.ts +28 -0
  37. package/dist/command/cli/status/status-lines.js +117 -0
  38. package/dist/command/cli.d.ts +1 -1
  39. package/dist/command/cli.js +29 -177
  40. package/dist/command/daemon/binding/daemon-status-probe.d.ts +32 -0
  41. package/dist/command/daemon/binding/daemon-status-probe.js +93 -0
  42. package/dist/command/daemon/binding/loopback-reach.d.ts +42 -0
  43. package/dist/command/daemon/binding/loopback-reach.js +63 -0
  44. package/dist/command/daemon/binding/port-presence.d.ts +1 -1
  45. package/dist/command/daemon/daemon-resilience.d.ts +13 -1
  46. package/dist/command/daemon/daemon-resilience.js +12 -1
  47. package/dist/command/daemon/daemon.d.ts +2 -11
  48. package/dist/command/daemon/daemon.js +8 -11
  49. package/dist/command/daemon/idle-shutdown.d.ts +2 -1
  50. package/dist/command/daemon/lifetime/heartbeat.d.ts +8 -0
  51. package/dist/command/daemon/lifetime/heartbeat.js +10 -0
  52. package/dist/command/daemon/lifetime/log-cap.d.ts +68 -0
  53. package/dist/command/daemon/lifetime/log-cap.js +115 -0
  54. package/dist/command/setup/agent-notice-memory.d.ts +16 -0
  55. package/dist/command/setup/agent-notice-memory.js +65 -0
  56. package/dist/command/setup/bringup/dev-server-wait.d.ts +1 -0
  57. package/dist/command/setup/bringup/dev-server-wait.js +16 -0
  58. package/dist/command/setup/bringup/ensure-daemon.js +1 -1
  59. package/dist/command/setup/init/init-host.js +8 -0
  60. package/dist/command/setup/init/init-runtime.js +13 -1
  61. package/dist/command/setup/node-effects.js +1 -1
  62. package/dist/command/setup/probe/page-probe.js +9 -2
  63. package/dist/command/setup/run-setup.d.ts +25 -7
  64. package/dist/command/setup/run-setup.js +51 -9
  65. package/dist/command/setup/setup-command.js +31 -3
  66. package/dist/command/setup/terminal/confirm.js +2 -1
  67. package/dist/command/update/updater.d.ts +27 -0
  68. package/dist/command/update/updater.js +35 -2
  69. package/dist/features/crawl/crawl-tools.js +3 -1
  70. package/dist/features/crawl/crawl.d.ts +2 -0
  71. package/dist/features/crawl/crawl.js +3 -1
  72. package/dist/features/harness/drive-report.d.ts +73 -0
  73. package/dist/features/harness/drive-report.js +196 -0
  74. package/dist/features/harness/driver.js +29 -8
  75. package/dist/features/harness/drivers.d.ts +19 -0
  76. package/dist/features/harness/drivers.js +19 -0
  77. package/dist/features/harness/fill-values.d.ts +67 -0
  78. package/dist/features/harness/fill-values.js +153 -0
  79. package/dist/features/harness/jev-driver.d.ts +188 -0
  80. package/dist/features/harness/jev-driver.js +661 -0
  81. package/dist/features/harness/openai-driver.d.ts +46 -0
  82. package/dist/features/harness/openai-driver.js +197 -0
  83. package/dist/features/harness/platform-config.d.ts +62 -0
  84. package/dist/features/harness/platform-config.js +97 -0
  85. package/dist/features/visual/visual-tools.js +26 -2
  86. package/dist/index.d.ts +1 -1
  87. package/dist/index.js +80 -130
  88. package/dist/judgement/capsule/capsule-store.d.ts +31 -2
  89. package/dist/judgement/capsule/capsule-store.js +58 -6
  90. package/dist/judgement/capsule/capsule.js +5 -0
  91. package/dist/judgement/capsule/causal-summary.d.ts +13 -1
  92. package/dist/judgement/capsule/causal-summary.js +7 -0
  93. package/dist/judgement/domain/domain-model.js +38 -9
  94. package/dist/judgement/domain/domain-tools.js +4 -2
  95. package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
  96. package/dist/judgement/phenomena/phenomena.d.ts +2 -1
  97. package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
  98. package/dist/judgement/runs/artifact/gap-report.js +46 -0
  99. package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
  100. package/dist/judgement/runs/artifact/gap-summary.js +85 -0
  101. package/dist/judgement/runs/artifact/render-report.js +4 -1
  102. package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
  103. package/dist/judgement/runs/artifact/to-artifact.js +1 -1
  104. package/dist/judgement/runs/context-tools.js +21 -3
  105. package/dist/judgement/runs/drive-run.js +7 -2
  106. package/dist/judgement/runs/verification-sync.d.ts +2 -2
  107. package/dist/judgement/runs/verification-sync.js +9 -2
  108. package/dist/language/flows/affected-tools.js +6 -3
  109. package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
  110. package/dist/language/flows/annotate-notes/annotate.js +66 -18
  111. package/dist/language/flows/flow-anchor.d.ts +12 -0
  112. package/dist/language/flows/flow-anchor.js +20 -0
  113. package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
  114. package/dist/language/flows/flow-expect-grammar.js +92 -11
  115. package/dist/language/flows/flow-intent.js +1 -1
  116. package/dist/language/flows/flow-learning.d.ts +3 -0
  117. package/dist/language/flows/flow-learning.js +39 -8
  118. package/dist/language/flows/flow-replay-run.d.ts +55 -10
  119. package/dist/language/flows/flow-replay-run.js +217 -58
  120. package/dist/language/flows/flow-replay-types.d.ts +9 -0
  121. package/dist/language/flows/flow-replay.d.ts +29 -11
  122. package/dist/language/flows/flow-replay.js +221 -203
  123. package/dist/language/flows/flow-result.d.ts +18 -1
  124. package/dist/language/flows/flow-result.js +18 -2
  125. package/dist/language/flows/flow-step-runners.d.ts +26 -10
  126. package/dist/language/flows/flow-step-runners.js +20 -4
  127. package/dist/language/flows/flow-store-for-session.d.ts +2 -1
  128. package/dist/language/flows/flow-success.d.ts +21 -6
  129. package/dist/language/flows/flow-success.js +50 -138
  130. package/dist/language/flows/flow-tools.js +8 -4
  131. package/dist/language/flows/flows.d.ts +15 -10
  132. package/dist/language/flows/flows.js +17 -4
  133. package/dist/language/flows/heal.d.ts +9 -1
  134. package/dist/language/flows/heal.js +24 -2
  135. package/dist/language/flows/learned-sources.d.ts +21 -0
  136. package/dist/language/flows/learned-sources.js +84 -0
  137. package/dist/language/flows/narrow-write.d.ts +4 -4
  138. package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
  139. package/dist/language/flows/recording/recording-backtrack.js +1 -1
  140. package/dist/language/flows/recording/replay-halt.d.ts +2 -1
  141. package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
  142. package/dist/language/flows/replay.js +13 -9
  143. package/dist/language/flows/stores/annotation-store.d.ts +5 -5
  144. package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
  145. package/dist/language/flows/stores/flake-store.js +2 -3
  146. package/dist/language/flows/suite/coverage.d.ts +1 -1
  147. package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
  148. package/dist/language/flows/suite-selection.d.ts +15 -1
  149. package/dist/language/flows/suite-selection.js +1 -1
  150. package/dist/language/flows/verify-change-tools.js +21 -4
  151. package/dist/memory/cloud/account-state.d.ts +1 -1
  152. package/dist/memory/cloud/account-state.js +5 -4
  153. package/dist/memory/cloud/cloud-config.d.ts +9 -0
  154. package/dist/memory/cloud/cloud-config.js +9 -0
  155. package/dist/memory/cloud/cloud-sync.d.ts +7 -4
  156. package/dist/memory/cloud/cloud-sync.js +11 -6
  157. package/dist/memory/cloud/harness-config.d.ts +12 -0
  158. package/dist/memory/cloud/harness-config.js +58 -0
  159. package/dist/memory/cloud/harness-offer.d.ts +44 -0
  160. package/dist/memory/cloud/harness-offer.js +146 -0
  161. package/dist/memory/cloud/harness-switch.d.ts +11 -0
  162. package/dist/memory/cloud/harness-switch.js +53 -0
  163. package/dist/memory/cloud/sync-cycle.d.ts +12 -0
  164. package/dist/memory/cloud/sync-cycle.js +29 -4
  165. package/dist/memory/cloud/sync-daemon.d.ts +35 -2
  166. package/dist/memory/cloud/sync-daemon.js +45 -0
  167. package/dist/memory/cloud/sync-disk.js +11 -6
  168. package/dist/memory/impact/impact-recorder.d.ts +8 -0
  169. package/dist/memory/impact/impact-recorder.js +2 -1
  170. package/dist/memory/impact/impact-store.d.ts +5 -0
  171. package/dist/memory/impact/impact-store.js +25 -0
  172. package/dist/memory/intent/inline-intent.js +8 -3
  173. package/dist/memory/intent/intent-shard-store.d.ts +33 -47
  174. package/dist/memory/intent/intent-shard-store.js +253 -141
  175. package/dist/memory/intent/intent-shard.d.ts +22 -23
  176. package/dist/memory/intent/intent-shard.js +1 -1
  177. package/dist/memory/intent/intent-store.d.ts +13 -8
  178. package/dist/memory/intent/intent-store.js +46 -90
  179. package/dist/memory/intent/intent-tools.js +34 -23
  180. package/dist/memory/journal/ambient-store.js +2 -3
  181. package/dist/memory/journal/attach-journal.d.ts +11 -0
  182. package/dist/memory/journal/attach-journal.js +1 -1
  183. package/dist/memory/journal/deviation-report.js +4 -1
  184. package/dist/memory/journal/deviation-service.js +10 -2
  185. package/dist/memory/journal/drive-flow.d.ts +2 -2
  186. package/dist/memory/journal/drive-flow.js +21 -12
  187. package/dist/memory/journal/envelope-store.js +2 -3
  188. package/dist/memory/journal/journal-query.d.ts +35 -1
  189. package/dist/memory/journal/journal-query.js +46 -0
  190. package/dist/memory/journal/journal-recorder.d.ts +71 -2
  191. package/dist/memory/journal/journal-recorder.js +45 -7
  192. package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
  193. package/dist/memory/journal/on-disk/retain-policy.js +44 -0
  194. package/dist/memory/journal/on-disk/retention.d.ts +91 -2
  195. package/dist/memory/journal/on-disk/retention.js +181 -11
  196. package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
  197. package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
  198. package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -2
  199. package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -38
  200. package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
  201. package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
  202. package/dist/memory/journal/route-template.d.ts +22 -0
  203. package/dist/memory/journal/route-template.js +44 -0
  204. package/dist/memory/journal/self-observation.d.ts +3 -0
  205. package/dist/memory/journal/self-observation.js +67 -0
  206. package/dist/memory/journal/session-end.d.ts +17 -4
  207. package/dist/memory/journal/session-end.js +85 -7
  208. package/dist/memory/journal/session-journal.d.ts +122 -5
  209. package/dist/memory/journal/session-journal.js +489 -32
  210. package/dist/memory/project/artifact-root-resolver.d.ts +3 -0
  211. package/dist/memory/project/artifact-root-resolver.js +89 -0
  212. package/dist/memory/project/artifact-root.d.ts +39 -2
  213. package/dist/memory/project/artifact-root.js +58 -1
  214. package/dist/memory/project/baselines.d.ts +1 -1
  215. package/dist/memory/project/dir/fill-value-store.d.ts +32 -0
  216. package/dist/memory/project/dir/fill-value-store.js +88 -0
  217. package/dist/memory/project/dir/reticle-dir.d.ts +15 -5
  218. package/dist/memory/project/dir/reticle-dir.js +11 -0
  219. package/dist/memory/project/fs/fs-port.d.ts +22 -1
  220. package/dist/memory/project/fs/fs-port.js +72 -5
  221. package/dist/memory/project/fs/write-atomic.d.ts +26 -0
  222. package/dist/memory/project/fs/write-atomic.js +48 -0
  223. package/dist/memory/project/learned-routes.d.ts +16 -2
  224. package/dist/memory/project/learned-routes.js +26 -10
  225. package/dist/memory/project/project-for-root.d.ts +30 -0
  226. package/dist/memory/project/project-for-root.js +43 -0
  227. package/dist/memory/project/project-tools.js +10 -5
  228. package/dist/memory/project/session-root.d.ts +35 -6
  229. package/dist/memory/project/session-root.js +70 -8
  230. package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
  231. package/dist/memory/recall/prior/connection-memory.js +8 -0
  232. package/dist/memory/recall/registered-projects.d.ts +2 -1
  233. package/dist/portal/bridge/auth-failure-reason.d.ts +9 -0
  234. package/dist/portal/bridge/auth-failure-reason.js +20 -3
  235. package/dist/portal/bridge/bridge.d.ts +6 -1
  236. package/dist/portal/bridge/bridge.js +36 -2
  237. package/dist/portal/input/real-input.js +2 -1
  238. package/dist/portal/pool/browser-pool.d.ts +10 -0
  239. package/dist/portal/pool/browser-pool.js +10 -0
  240. package/dist/portal/pool/playwright-launcher.js +1 -0
  241. package/dist/portal/pool/pool-contract.d.ts +2 -0
  242. package/dist/portal/pool/storage-seed.d.ts +2 -1
  243. package/dist/portal/pool/zero-install.d.ts +27 -0
  244. package/dist/portal/pool/zero-install.js +54 -0
  245. package/dist/portal/realm/web-realm.js +6 -0
  246. package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
  247. package/dist/portal/session/dev-server/served-document.d.ts +21 -0
  248. package/dist/portal/session/dev-server/served-document.js +57 -0
  249. package/dist/portal/session/facts/background-traffic.d.ts +9 -0
  250. package/dist/portal/session/facts/background-traffic.js +25 -0
  251. package/dist/portal/session/facts/session-replaced.d.ts +19 -0
  252. package/dist/portal/session/facts/session-replaced.js +22 -0
  253. package/dist/portal/session/live-control-tools.js +48 -3
  254. package/dist/portal/session/no-session-diagnosis.d.ts +1 -0
  255. package/dist/portal/session/no-session-diagnosis.js +67 -60
  256. package/dist/portal/session/no-session-next-action.js +3 -2
  257. package/dist/portal/session/no-session-watch.js +14 -10
  258. package/dist/portal/session/presence/attachment-history.d.ts +13 -2
  259. package/dist/portal/session/rebind.d.ts +18 -0
  260. package/dist/portal/session/rebind.js +38 -0
  261. package/dist/portal/session/session-health.d.ts +61 -0
  262. package/dist/portal/session/session-health.js +40 -1
  263. package/dist/portal/session/session-info.d.ts +6 -0
  264. package/dist/portal/session/session-manager.d.ts +13 -0
  265. package/dist/portal/session/session-manager.js +42 -3
  266. package/dist/portal/session/session-successor.d.ts +11 -0
  267. package/dist/portal/session/session-successor.js +14 -1
  268. package/dist/portal/session/session-verdict-facts.d.ts +28 -0
  269. package/dist/portal/session/session-verdict-facts.js +15 -0
  270. package/dist/portal/session/session.d.ts +26 -10
  271. package/dist/portal/session/session.js +48 -49
  272. package/dist/status-payload.d.ts +12 -1
  273. package/dist/status-payload.js +4 -1
  274. package/dist/surface/http-server.js +3 -5
  275. package/dist/surface/mcp/faults/mcp-outage.d.ts +2 -1
  276. package/dist/surface/mcp/faults/mcp-outage.js +20 -4
  277. package/dist/surface/mcp/mcp-proxy.d.ts +10 -1
  278. package/dist/surface/mcp/mcp-proxy.js +23 -12
  279. package/dist/surface/mcp/mcp.d.ts +10 -1
  280. package/dist/surface/mcp/mcp.js +59 -29
  281. package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +15 -2
  282. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +22 -5
  283. package/dist/surface/mcp/proxy/proxy-log.d.ts +5 -11
  284. package/dist/surface/mcp/proxy/proxy-log.js +7 -22
  285. package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
  286. package/dist/surface/mcp/server-instructions.js +12 -1
  287. package/dist/surface/tools/act/act-preflight.js +6 -1
  288. package/dist/surface/tools/act/already-true.d.ts +40 -0
  289. package/dist/surface/tools/act/already-true.js +36 -0
  290. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
  291. package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
  292. package/dist/surface/tools/act/sequence-grade.js +13 -0
  293. package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
  294. package/dist/surface/tools/act/settle-in-flight.js +29 -6
  295. package/dist/surface/tools/act-capsule.js +49 -8
  296. package/dist/surface/tools/act-tools.js +25 -24
  297. package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
  298. package/dist/surface/tools/args/numeric-bounds.js +30 -3
  299. package/dist/surface/tools/assert/assert-grade.js +14 -2
  300. package/dist/surface/tools/assert/assert-verdict.js +74 -21
  301. package/dist/surface/tools/browser-tools.js +4 -3
  302. package/dist/surface/tools/coverage-tools.js +5 -2
  303. package/dist/surface/tools/dynamic-tools.js +43 -46
  304. package/dist/surface/tools/error-recovery.d.ts +6 -0
  305. package/dist/surface/tools/error-recovery.js +15 -0
  306. package/dist/surface/tools/explore-tools.js +80 -7
  307. package/dist/surface/tools/feedback-tools.d.ts +2 -1
  308. package/dist/surface/tools/feedback-tools.js +50 -3
  309. package/dist/surface/tools/first-sentence.d.ts +24 -0
  310. package/dist/surface/tools/first-sentence.js +0 -0
  311. package/dist/surface/tools/harness-explore.d.ts +125 -1
  312. package/dist/surface/tools/harness-explore.js +401 -15
  313. package/dist/surface/tools/harness-plan.d.ts +76 -0
  314. package/dist/surface/tools/harness-plan.js +156 -0
  315. package/dist/surface/tools/harness-toolset.js +12 -1
  316. package/dist/surface/tools/invoke-tool.js +82 -9
  317. package/dist/surface/tools/lease-hint.d.ts +8 -0
  318. package/dist/surface/tools/lease-hint.js +10 -0
  319. package/dist/surface/tools/lease-tools.d.ts +16 -28
  320. package/dist/surface/tools/lease-tools.js +53 -5
  321. package/dist/surface/tools/lineage-tools.js +1 -1
  322. package/dist/surface/tools/live-call-text.d.ts +6 -0
  323. package/dist/surface/tools/live-call-text.js +36 -2
  324. package/dist/surface/tools/observe-tools.js +69 -15
  325. package/dist/surface/tools/read/query-windowed.d.ts +1 -1
  326. package/dist/surface/tools/read/query-windowed.js +1 -1
  327. package/dist/surface/tools/read-tools.js +1 -2
  328. package/dist/surface/tools/real-input-attempt.d.ts +0 -5
  329. package/dist/surface/tools/real-input-attempt.js +1 -1
  330. package/dist/surface/tools/tool-kit.d.ts +23 -3
  331. package/dist/surface/tools/tool-kit.js +6 -1
  332. package/dist/surface/tools/tool-surface.js +17 -5
  333. package/dist/surface/tools/tools-helpers.d.ts +8 -7
  334. package/dist/surface/tools/tools-helpers.js +11 -13
  335. package/dist/surface/tools/tools.js +70 -30
  336. package/dist/telemetry/bug-found.js +32 -2
  337. package/dist/telemetry/feedback-context.js +3 -3
  338. package/dist/telemetry/feedback-local.d.ts +1 -0
  339. package/dist/telemetry/feedback-local.js +1 -3
  340. package/dist/telemetry/feedback.d.ts +1 -1
  341. package/dist/telemetry/harness-drive.d.ts +6 -0
  342. package/dist/telemetry/harness-drive.js +39 -0
  343. package/dist/telemetry/hud-metrics.d.ts +32 -0
  344. package/dist/telemetry/hud-metrics.js +95 -0
  345. package/dist/telemetry/mcp-connection.d.ts +14 -1
  346. package/dist/telemetry/mcp-connection.js +17 -2
  347. package/dist/telemetry/onboarding-firsts.d.ts +30 -1
  348. package/dist/telemetry/onboarding-firsts.js +54 -12
  349. package/dist/telemetry/session-metrics.d.ts +5 -0
  350. package/dist/telemetry/session-metrics.js +15 -1
  351. package/dist/telemetry/verification-of.js +31 -3
  352. package/dist/wire-journal.d.ts +31 -0
  353. package/dist/wire-journal.js +100 -0
  354. package/docs/README.md +1 -0
  355. package/docs/agent-cheatsheet.md +7 -3
  356. package/docs/autodrive.md +42 -2
  357. package/docs/benchmarks.md +1 -1
  358. package/docs/capabilities.mdx +1 -1
  359. package/docs/cli/cloud.mdx +11 -9
  360. package/docs/cli/doctor.mdx +1 -0
  361. package/docs/cli/init.mdx +1 -0
  362. package/docs/cli/report.mdx +44 -0
  363. package/docs/cli/status.mdx +35 -20
  364. package/docs/cli.mdx +8 -18
  365. package/docs/desktop.mdx +1 -1
  366. package/docs/faq.mdx +1 -1
  367. package/docs/flows.md +11 -0
  368. package/docs/frameworks.mdx +113 -1
  369. package/docs/install-agentic.mdx +36 -0
  370. package/docs/install-manual.mdx +1 -1
  371. package/docs/packages/tauri.mdx +1 -1
  372. package/docs/packages.mdx +1 -1
  373. package/docs/predicates.mdx +144 -4
  374. package/docs/system-map.md +6 -4
  375. package/docs/telemetry.md +2 -1
  376. package/docs/token-efficiency.md +2 -2
  377. package/docs/tools/overview.mdx +1 -1
  378. package/docs/tools/query.mdx +10 -10
  379. package/docs/troubleshooting.mdx +8 -2
  380. package/docs/usage.md +2 -2
  381. package/docs/what-is-recorded.md +62 -0
  382. package/package.json +5 -5
  383. package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
  384. package/dist/judgement/outcome/predicate-to-expect.js +0 -148
@@ -7,6 +7,7 @@
7
7
  * 3..reticle.json "port" field in the cwd ← per-project isolation
8
8
  * 4. RETICLE_DEFAULT_PORT (4400)
9
9
  */
10
+ import { type ProjectId } from '@reticlehq/core';
10
11
  /** The project config `reticle init` writes. */
11
12
  export declare const RETICLE_CONFIG_BASENAME = ".reticle.json";
12
13
  /**
@@ -39,7 +40,20 @@ export declare function readProjectPort(cwd: string): number | undefined;
39
40
  * uses it as the default resolve scope so auto-selection stays within the active app. Returns
40
41
  * undefined if the file is absent/unreadable or has no non-empty string projectId.
41
42
  */
42
- export declare function readProjectId(cwd: string): string | undefined;
43
+ export declare function readProjectId(cwd: string): ProjectId | undefined;
44
+ /**
45
+ * Every project id this directory can speak for, for "has THIS project ever connected".
46
+ *
47
+ * `readProjectId` knows only what `init` wrote. The build plugins derive the same id from the package
48
+ * name and the app root WITHOUT writing `.reticle.json`, so a monorepo root (the app lives in
49
+ * `apps/web`) and an app wired by the plugin alone both read `undefined`, and the handshake told a
50
+ * working project nothing had ever connected. Deriving the plugin's own id cannot reopen the opposite
51
+ * lie: an id only counts once an app with exactly that id has dialled in.
52
+ *
53
+ * Workspace discovery is init's `findWorkspaceApps`, reused so the two cannot disagree about which
54
+ * directories are apps. Best-effort: a directory it cannot read contributes nothing.
55
+ */
56
+ export declare function projectIdsAt(cwd: string): ProjectId[];
43
57
  /**
44
58
  * The framework `reticle init` stamped into `.reticle.json`, when there is one.
45
59
  *
@@ -8,7 +8,9 @@
8
8
  * 4. RETICLE_DEFAULT_PORT (4400)
9
9
  */
10
10
  import { readFileSync } from 'node:fs';
11
- import { dirname, resolve } from 'node:path';
11
+ import { dirname, join, resolve } from 'node:path';
12
+ import { SILENT_HOST, buildNodeIo, deriveProjectId, findWorkspaceApps, packageName, } from '@reticlehq/init';
13
+ import { asProjectId } from '@reticlehq/core';
12
14
  /** The project config `reticle init` writes. */
13
15
  export const RETICLE_CONFIG_BASENAME = '.reticle.json';
14
16
  /**
@@ -76,10 +78,56 @@ export function readProjectPort(cwd) {
76
78
  */
77
79
  export function readProjectId(cwd) {
78
80
  const id = findProjectConfig(cwd)?.['projectId'];
81
+ // MINTED HERE. The other provenance boundary: the `projectId` key of a project's own
82
+ // `.reticle.json`. Like HELLO's field, it is a project's id because of WHERE it was read, not
83
+ // because of what it looks like.
79
84
  if ('string' === typeof id && id.length > 0)
80
- return id;
85
+ return asProjectId(id);
81
86
  return undefined;
82
87
  }
88
+ const PACKAGE_JSON = 'package.json';
89
+ /** The package name in `dir/package.json`, or undefined when there is no readable one. */
90
+ function packageNameAt(dir) {
91
+ try {
92
+ return packageName(JSON.parse(readFileSync(join(dir, PACKAGE_JSON), 'utf8')));
93
+ }
94
+ catch {
95
+ return undefined;
96
+ }
97
+ }
98
+ /** The id an app at `dir` carries: the one `init` recorded, else the one the build plugin derives. */
99
+ function projectIdFor(dir) {
100
+ return readProjectId(dir) ?? asProjectId(deriveProjectId(packageNameAt(dir), dir));
101
+ }
102
+ /**
103
+ * Every project id this directory can speak for, for "has THIS project ever connected".
104
+ *
105
+ * `readProjectId` knows only what `init` wrote. The build plugins derive the same id from the package
106
+ * name and the app root WITHOUT writing `.reticle.json`, so a monorepo root (the app lives in
107
+ * `apps/web`) and an app wired by the plugin alone both read `undefined`, and the handshake told a
108
+ * working project nothing had ever connected. Deriving the plugin's own id cannot reopen the opposite
109
+ * lie: an id only counts once an app with exactly that id has dialled in.
110
+ *
111
+ * Workspace discovery is init's `findWorkspaceApps`, reused so the two cannot disagree about which
112
+ * directories are apps. Best-effort: a directory it cannot read contributes nothing.
113
+ */
114
+ export function projectIdsAt(cwd) {
115
+ const recorded = readProjectId(cwd);
116
+ if (recorded !== undefined)
117
+ return [recorded];
118
+ const ids = [];
119
+ if (packageNameAt(cwd) !== undefined)
120
+ ids.push(projectIdFor(cwd));
121
+ try {
122
+ for (const app of findWorkspaceApps(buildNodeIo(cwd, SILENT_HOST))) {
123
+ ids.push(projectIdFor(join(cwd, app)));
124
+ }
125
+ }
126
+ catch {
127
+ // Discovery touches the filesystem; an unreadable tree is simply no further evidence.
128
+ }
129
+ return [...new Set(ids)];
130
+ }
83
131
  /**
84
132
  * The framework `reticle init` stamped into `.reticle.json`, when there is one.
85
133
  *
@@ -0,0 +1,16 @@
1
+ export interface ReportInput {
2
+ cwd: string;
3
+ now: number;
4
+ /** Did the working tree change? The hook stays silent on a turn that edited nothing. */
5
+ treeChanged: boolean;
6
+ session?: string;
7
+ hook?: boolean;
8
+ }
9
+ export declare function reportFor(input: ReportInput): Promise<{
10
+ lines: string[];
11
+ }>;
12
+ /**
13
+ * The command. As a hook it prints Claude Code's `systemMessage` shape and always exits 0: it is
14
+ * print-only by design, so the human sees the gap and the agent is never blocked by it.
15
+ */
16
+ export declare function handleReport(session: string | undefined, hook: boolean): Promise<void>;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * `reticle report [--session <id>] [--hook]` — what this project's latest session claimed, and what held.
3
+ *
4
+ * Read from disk, with no daemon: the journal is on disk, and a Stop hook has to answer in the second
5
+ * after the agent stops, when there may be nothing running to ask. The fold is `gapSummary`, the same
6
+ * one `reticle_context` serves, so the terminal and the agent cannot be told two different stories.
7
+ */
8
+ import { execFileSync } from 'node:child_process';
9
+ import { existsSync, readdirSync, statSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { DiscoveryInvite, ReticleDir } from '@reticlehq/core';
12
+ import { gapSummary } from '../../judgement/runs/artifact/gap-summary.js';
13
+ import { SessionJournal } from '../../memory/journal/session-journal.js';
14
+ import { createNodeFileSystem } from '../../memory/project/fs/fs-port.js';
15
+ import { gapHookLine, gapReportLines } from '../../judgement/runs/artifact/gap-report.js';
16
+ /**
17
+ * A session last written longer ago than this is not the turn a hook is asking about.
18
+ *
19
+ * ponytail: a fixed window, not a turn boundary — the Stop hook is not told when the turn began. A
20
+ * drive older than an hour reads as "no claim this session", which errs toward saying so.
21
+ */
22
+ const HOOK_WINDOW_MS = 60 * 60 * 1000;
23
+ /** The session whose action ledger was written most recently, if any. */
24
+ function newestSession(root) {
25
+ const sessionsDir = join(root, ReticleDir.SESSIONS_SUBDIR);
26
+ if (!existsSync(sessionsDir))
27
+ return undefined;
28
+ let newest;
29
+ for (const id of readdirSync(sessionsDir)) {
30
+ const ledger = join(sessionsDir, id, ReticleDir.JOURNAL_ACTIONS_FILE);
31
+ if (!existsSync(ledger))
32
+ continue;
33
+ const writtenAt = statSync(ledger).mtimeMs;
34
+ if (newest === undefined || writtenAt > newest.writtenAt)
35
+ newest = { id, writtenAt };
36
+ }
37
+ return newest;
38
+ }
39
+ export async function reportFor(input) {
40
+ if (true === input.hook && !input.treeChanged)
41
+ return { lines: [] };
42
+ const root = join(input.cwd, ReticleDir.ROOT);
43
+ const found = input.session === undefined ? newestSession(root) : { id: input.session, writtenAt: input.now };
44
+ const current = found !== undefined && (true !== input.hook || input.now - found.writtenAt <= HOOK_WINDOW_MS)
45
+ ? found
46
+ : undefined;
47
+ const actions = current === undefined
48
+ ? []
49
+ : await new SessionJournal(createNodeFileSystem(), root, current.id).readActions();
50
+ const gap = gapSummary(actions);
51
+ if (true === input.hook) {
52
+ const line = gapHookLine(gap);
53
+ // A finish with nothing proved is the moment Reticle let somebody down: say who to tell.
54
+ return { lines: line === undefined ? [] : [line, DiscoveryInvite.HUMAN] };
55
+ }
56
+ const header = current === undefined ? [] : [`session ${current.id}`];
57
+ return { lines: [...header, ...gapReportLines(gap), DiscoveryInvite.HUMAN] };
58
+ }
59
+ /** Uncommitted changes in `cwd`. Outside a git repository nothing can say, so it answers yes. */
60
+ function treeChanged(cwd) {
61
+ try {
62
+ return '' !== execFileSync('git', ['status', '--porcelain'], { cwd, encoding: 'utf8' }).trim();
63
+ }
64
+ catch {
65
+ return true;
66
+ }
67
+ }
68
+ /**
69
+ * The command. As a hook it prints Claude Code's `systemMessage` shape and always exits 0: it is
70
+ * print-only by design, so the human sees the gap and the agent is never blocked by it.
71
+ */
72
+ export async function handleReport(session, hook) {
73
+ const cwd = process.cwd();
74
+ let lines;
75
+ try {
76
+ ({ lines } = await reportFor({
77
+ cwd,
78
+ now: Date.now(),
79
+ treeChanged: hook ? treeChanged(cwd) : true,
80
+ ...(session === undefined ? {} : { session }),
81
+ hook,
82
+ }));
83
+ }
84
+ catch (error) {
85
+ // A hook that throws turns the agent's stop into an error the user did not cause; it says nothing
86
+ // instead. Run by hand, the reason is the answer (an id that is not a session, a journal it
87
+ // could not read), so it is printed and the exit is non-zero.
88
+ if (hook)
89
+ return;
90
+ process.stderr.write(`reticle report: ${error instanceof Error ? error.message : String(error)}\n`);
91
+ process.exitCode = 1;
92
+ return;
93
+ }
94
+ if (0 === lines.length)
95
+ return;
96
+ process.stdout.write(hook ? `${JSON.stringify({ systemMessage: lines.join('\n') })}\n` : `${lines.join('\n')}\n`);
97
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * `reticle status`, rendered for a person.
3
+ *
4
+ * The command printed one line of JSON and nothing else. Its own source called it "the second-most
5
+ * run command and the one a HUMAN types" — and the answer to that human was
6
+ * `{"t":"…","event":"reticle_status","port":4400,"running":true,…}`. It is worse than that now that
7
+ * agents run it: an agent's terminal output is read by the person sitting behind it, so a machine
8
+ * line is a line two readers have to decode and only one of them can.
9
+ *
10
+ * The JSON stays, unchanged, on stderr — it is documented, and it is what a log is for. This is the
11
+ * same facts on stdout, in the shape `doctor` already uses, so the two commands read as one tool.
12
+ *
13
+ * Pure, and tolerant: it is handed the very object the log line is built from, narrows it without
14
+ * trusting it, and prints what it can recognise. A status that cannot be rendered must still be
15
+ * logged, so nothing here may throw.
16
+ */
17
+ /** Every row label this block can print. The one spelling; nothing writes a label inline. */
18
+ export declare const StatusLabel: {
19
+ readonly DAEMON: "daemon";
20
+ readonly SESSIONS: "sessions";
21
+ readonly AGENT_LINK: "agent link";
22
+ readonly NEXT: "next";
23
+ readonly SPLIT_BRAIN: "split brain";
24
+ readonly UPDATE: "update";
25
+ };
26
+ export type StatusLabel = (typeof StatusLabel)[keyof typeof StatusLabel];
27
+ /** The block to print on stdout, one string per line, heading first. */
28
+ export declare function statusLines(fields: Record<string, unknown>): string[];
@@ -0,0 +1,117 @@
1
+ /**
2
+ * `reticle status`, rendered for a person.
3
+ *
4
+ * The command printed one line of JSON and nothing else. Its own source called it "the second-most
5
+ * run command and the one a HUMAN types" — and the answer to that human was
6
+ * `{"t":"…","event":"reticle_status","port":4400,"running":true,…}`. It is worse than that now that
7
+ * agents run it: an agent's terminal output is read by the person sitting behind it, so a machine
8
+ * line is a line two readers have to decode and only one of them can.
9
+ *
10
+ * The JSON stays, unchanged, on stderr — it is documented, and it is what a log is for. This is the
11
+ * same facts on stdout, in the shape `doctor` already uses, so the two commands read as one tool.
12
+ *
13
+ * Pure, and tolerant: it is handed the very object the log line is built from, narrows it without
14
+ * trusting it, and prints what it can recognise. A status that cannot be rendered must still be
15
+ * logged, so nothing here may throw.
16
+ */
17
+ /** Every row label this block can print. The one spelling; nothing writes a label inline. */
18
+ export const StatusLabel = {
19
+ DAEMON: 'daemon',
20
+ SESSIONS: 'sessions',
21
+ AGENT_LINK: 'agent link',
22
+ NEXT: 'next',
23
+ SPLIT_BRAIN: 'split brain',
24
+ UPDATE: 'update',
25
+ };
26
+ const LABELS = Object.values(StatusLabel);
27
+ /** The heading, so a reader can tell which command produced the block. */
28
+ const HEADING = 'reticle status';
29
+ const INDENT = ' ';
30
+ /** Padded off the longest label, for the same reason `doctor` does it: the column IS the legibility. */
31
+ const LABEL_COLUMN = LABELS.reduce((widest, l) => Math.max(widest, l.length), 0) + 2;
32
+ const OK = '✓';
33
+ const NOT_OK = '✗';
34
+ function row(label, rest) {
35
+ return `${INDENT}${label.padEnd(LABEL_COLUMN)}${rest}`;
36
+ }
37
+ /** A continuation line under a row: indented past the label column, carrying no label of its own. */
38
+ function continuation(rest) {
39
+ return `${INDENT}${' '.repeat(LABEL_COLUMN)}${rest}`;
40
+ }
41
+ function str(fields, key) {
42
+ const v = fields[key];
43
+ return 'string' === typeof v && v.length > 0 ? v : undefined;
44
+ }
45
+ function num(fields, key) {
46
+ const v = fields[key];
47
+ return 'number' === typeof v ? v : undefined;
48
+ }
49
+ function sessionViews(fields) {
50
+ const raw = fields['sessions'];
51
+ if (!Array.isArray(raw))
52
+ return [];
53
+ return raw.flatMap((s) => {
54
+ if ('object' !== typeof s || null === s)
55
+ return [];
56
+ const r = s;
57
+ // Each of these explains something a reader would otherwise call a bug: a throttled or hidden
58
+ // tab answers slowly, a stale one answers about a page that has moved on, and an unresponsive
59
+ // one does not answer at all.
60
+ const notes = [
61
+ true === r['throttled'] ? 'throttled' : undefined,
62
+ true === r['hidden'] ? 'hidden' : undefined,
63
+ true === r['stale'] ? 'stale' : undefined,
64
+ true === r['unresponsive'] ? 'unresponsive' : undefined,
65
+ ].filter((n) => n !== undefined);
66
+ return [{ url: str(r, 'url') ?? '(no url reported)', notes }];
67
+ });
68
+ }
69
+ /** The block to print on stdout, one string per line, heading first. */
70
+ export function statusLines(fields) {
71
+ const port = num(fields, 'port');
72
+ const at = port === undefined ? '' : ` on :${String(port)}`;
73
+ const pid = num(fields, 'pid');
74
+ const running = true === fields['running'];
75
+ const lines = [HEADING];
76
+ lines.push(row(StatusLabel.DAEMON, running
77
+ ? `${OK} running${at}${pid === undefined ? '' : ` (pid ${String(pid)})`}`
78
+ : `${NOT_OK} not running${at}`));
79
+ // What was actually ON the port, when the pid file said nothing useful. "A stranger holds this"
80
+ // and "nothing is here" were the same output once, and the true sentence was said by nothing a
81
+ // person sees — so the word belongs in the prose, not only in the event.
82
+ const presence = str(fields, 'presence');
83
+ if (presence !== undefined)
84
+ lines.push(continuation(`port presence: ${presence}`));
85
+ // The refusal a FOREIGN port produces. It is the whole answer when it is present, so it rides the
86
+ // daemon row rather than waiting for a `next` that is not built in that branch.
87
+ const reason = str(fields, 'reason');
88
+ if (reason !== undefined)
89
+ lines.push(continuation(reason));
90
+ if (running) {
91
+ const count = num(fields, 'sessionCount') ?? 0;
92
+ lines.push(row(StatusLabel.SESSIONS, count > 0
93
+ ? `${OK} ${String(count)} page${1 === count ? '' : 's'} connected`
94
+ : `${NOT_OK} no page connected`));
95
+ for (const s of sessionViews(fields)) {
96
+ lines.push(continuation(`${s.url}${0 === s.notes.length ? '' : ` (${s.notes.join(', ')})`}`));
97
+ }
98
+ // The daemon's own explanation for an empty session list, which outranks anything this command
99
+ // could infer — it is the one holding the connection.
100
+ const why = str(fields, 'why');
101
+ if (why !== undefined)
102
+ lines.push(continuation(why));
103
+ }
104
+ const client = str(fields, 'mcpClient');
105
+ if (client !== undefined)
106
+ lines.push(row(StatusLabel.AGENT_LINK, client));
107
+ for (const [label, key] of [
108
+ [StatusLabel.NEXT, 'nextAction'],
109
+ [StatusLabel.SPLIT_BRAIN, 'splitBrain'],
110
+ [StatusLabel.UPDATE, 'updateAvailable'],
111
+ ]) {
112
+ const value = str(fields, key);
113
+ if (value !== undefined)
114
+ lines.push(row(label, value));
115
+ }
116
+ return lines;
117
+ }
@@ -2,5 +2,5 @@
2
2
  import { parseCliArgs, CLI_USAGE } from './cli/cli-parse.js';
3
3
  export { parseCliArgs, CLI_USAGE };
4
4
  export type { CliResult } from './cli/cli-parse.js';
5
- export declare function handleStatus(port: number): Promise<void>;
5
+ export declare function handleStatus(port: number, json?: boolean): Promise<void>;
6
6
  export declare function main(): void;
@@ -8,15 +8,15 @@ import { openFailureNote } from './cli/answers/open-note.js';
8
8
  import { realpathSync } from 'node:fs';
9
9
  import { join } from 'node:path';
10
10
  import { installCrashGuard } from './setup/setup-command.js';
11
- import { stateDirProblem } from './daemon/state-dir.js';
12
11
  import { readDevServers } from './daemon/dev-servers.js';
13
12
  import { hasAnyAppConnectedBefore, hasProjectConnectedBefore, } from '../memory/recall/prior/connection-memory.js';
14
13
  import { attachStatusFields } from '../surface/mcp/attach-memory.js';
15
14
  import { splitBrainFields, withNextAction } from './cli/status-fields.js';
16
15
  import { reticleStateHome } from './daemon/daemon.js';
17
16
  import { handleMcp } from './cli/mcp-command.js';
17
+ import { handleReport } from './cli/report-command.js';
18
18
  import { resolveDaemonForProject } from './daemon/daemon-resolve.js';
19
- import { daemonSpawnArgs, daemonStartOptions } from './cli/daemon-start-options.js';
19
+ import { daemonStartOptions } from './cli/daemon-start-options.js';
20
20
  import { handleWatch, handleCapsules, handleGate, loadNamedFlows, resolveChangedFiles, } from './cli/cli-flow-commands.js';
21
21
  import { RETICLE_DEFAULT_PORT, ReticleDir, ReticleEnv, devServersForProject, } from '@reticlehq/core';
22
22
  import { loadDotEnv } from '../telemetry/dev-repo.js';
@@ -26,20 +26,20 @@ import { createNodeFileSystem } from '../memory/project/fs/fs-port.js';
26
26
  import { affectedSavedFlows } from '../language/flows/change/flow-sources.js';
27
27
  import { availableUpdate } from './update/update-nudge.js';
28
28
  import { handleUpdate, handleRollback } from './cli/cli-update-commands.js';
29
- import { startDaemon, RETICLE_VERIFY_DEFAULT_PORT } from '../index.js';
30
- import { verifyEndpointMismatch } from '../status-payload.js';
29
+ import { startDaemon } from '../index.js';
31
30
  import { isCloudCommand, runCloudCommand } from './cli/cloud-cli.js';
32
31
  import { SERVER_VERSION } from './version/identity/server-version.js';
33
32
  import { log } from '../log.js';
34
- import { readPid, isAlive, removePid, spawnDaemon, discoverDaemonPort, writeDaemonRegistry, logPath, } from './daemon/daemon.js';
33
+ import { readPid, isAlive, removePid, spawnDaemon, discoverDaemonPort, writeDaemonRegistry, } from './daemon/daemon.js';
35
34
  import { PortPresence, probePresence, presenceIsUsable, describePresence, } from './daemon/binding/port-presence.js';
36
35
  import { waitForDaemon, probeDaemon } from '../surface/mcp/mcp-proxy.js';
37
36
  import { installDaemonResilience, recordExitReason, DaemonExitReason, } from './daemon/daemon-resilience.js';
38
37
  import { IdleShutdown, resolveIdleShutdownMs, resolveIdleCheckMs } from './daemon/idle-shutdown.js';
39
38
  import { DaemonHeartbeat, resolveHeartbeatMs } from './daemon/lifetime/heartbeat.js';
40
39
  import { everServedToolCall } from './daemon/lifetime/daemon-usefulness.js';
41
- import { DAEMON_START_FAILED_EVENT, readDaemonStartupCause, } from './daemon/lifetime/startup-failure.js';
42
- import { fetchStatus, summarizeStatus, warnOnDaemonSkew, decideOpen, openInBrowser, openCommand, } from './cli/launch/cli-launch.js';
40
+ import { DAEMON_START_FAILED_EVENT } from './daemon/lifetime/startup-failure.js';
41
+ import { summarizeStatus, warnOnDaemonSkew, decideOpen, openInBrowser, openCommand, } from './cli/launch/cli-launch.js';
42
+ import { fetchStatus } from './daemon/binding/daemon-status-probe.js';
43
43
  import { handleDrive } from './cli/drive/drive-command.js';
44
44
  import { handleVerify } from './cli/cli-verify.js';
45
45
  import { runKill } from './cli/cli-kill.js';
@@ -70,6 +70,7 @@ function handleInit(parsed) {
70
70
  // past a missing package manager, and could not honour that while never being told about it.
71
71
  ...(parsed.url === undefined ? {} : { url: parsed.url }),
72
72
  captureBodies: true === parsed.captureBodies,
73
+ hooks: true === parsed.hooks,
73
74
  // The outcome is reported by confirmInstall instead, once it knows whether an app connected —
74
75
  // `init` writing files was never the same thing as `init` working (#269).
75
76
  deferOutcome: true,
@@ -77,178 +78,26 @@ function handleInit(parsed) {
77
78
  }, io);
78
79
  void continueAfterInit(parsed, result, io, cwd);
79
80
  }
80
- function handleServe(parsed) {
81
- void serveWithHonestExit(parsed);
82
- }
81
+ // `serve`, `stop` and `restart` live in `cli/lifecycle/daemon-lifecycle.ts`: one idea, and the
82
+ // largest group this file could give up without splitting something that belongs together.
83
+ import { handleServe, handleStop, handleRestart } from './cli/lifecycle/daemon-lifecycle.js';
84
+ import { statusLines } from './cli/status/status-lines.js';
83
85
  /**
84
- * Report the BIND, not the spawn.
85
- *
86
- * `serve` used to log `reticle_daemon_spawned` and exit 0 unconditionally, while the child died at
87
- * `reticle_daemon_start_failed` with an EADDRINUSE nothing joined back to the parent. Three surfaces
88
- * then disagreed about what was true and none of them named the port.
86
+ * Log the status event AND print it for a person.
89
87
  *
90
- * Two changes, both about saying what is actually the case: refuse up front when the port is held by
91
- * something that is not a Reticle daemon (a spawn there cannot succeed), and after spawning, wait
92
- * for the daemon to actually answer before claiming it started.
88
+ * Both, not either: the JSON line is documented and is what a log is for, and the block is for
89
+ * whoever is reading the terminal — which, once an agent is running the command, is somebody who
90
+ * did not type it and cannot be assumed to parse it.
93
91
  */
94
- async function serveWithHonestExit(parsed) {
95
- const presence = await probePresence(parsed.port, {
96
- tcpOpen: probeDaemon,
97
- status: fetchStatus,
98
- });
99
- if (presence === PortPresence.DAEMON) {
100
- // A daemon that is already up was started with ITS flags, not these — `serve` only attaches.
101
- // Exiting 0 here regardless is how `--http-port` came to be accepted and ignored (#687): the
102
- // running daemon kept serving whatever it was started with, and the flag vanished without a
103
- // word. Ask the daemon which verify port it actually serves and refuse when it is not the one
104
- // requested — a flag that cannot be honoured must say so, not report success.
105
- if (parsed.http) {
106
- const mismatch = verifyEndpointMismatch(await fetchStatus(parsed.port), parsed.httpPort ?? RETICLE_VERIFY_DEFAULT_PORT);
107
- if (mismatch !== undefined) {
108
- log('reticle_daemon_start_refused', { port: parsed.port, reason: mismatch });
109
- process.stderr.write(`${mismatch}\n`);
110
- process.exit(1);
111
- return;
112
- }
113
- }
114
- log('reticle_daemon_already_running', { port: parsed.port });
115
- return;
116
- }
117
- if (presence === PortPresence.FOREIGN) {
118
- log('reticle_daemon_start_refused', {
119
- port: parsed.port,
120
- presence,
121
- reason: describePresence(presence, parsed.port),
122
- });
123
- process.stderr.write(`${describePresence(presence, parsed.port)}\n`);
124
- process.exit(1);
92
+ function reportStatus(fields, json) {
93
+ if (json) {
94
+ log('reticle_status', fields);
125
95
  return;
126
96
  }
127
- const scriptPath = process.argv[1];
128
- if (scriptPath === undefined) {
129
- log('reticle_serve_no_script', {});
130
- process.exit(1);
131
- return;
132
- }
133
- const daemonArgs = daemonSpawnArgs(parsed);
134
- const startupStartedAt = Date.now();
135
- spawnDaemon(process.execPath, scriptPath, daemonArgs, parsed.port);
136
- // The child binds asynchronously and, when it cannot, exits 1 long after this process would have
137
- // reported success. Wait for it to ANSWER — `/status` responding is the only evidence a daemon
138
- // exists that does not come from the pid file the child may never have earned.
139
- const bound = await waitForPresence(parsed.port, PortPresence.DAEMON, SERVE_BIND_TIMEOUT_MS);
140
- if (!bound) {
141
- const settled = await probePresence(parsed.port, { tcpOpen: probeDaemon, status: fetchStatus });
142
- const startupCause = readDaemonStartupCause(logPath(parsed.port), startupStartedAt);
143
- log('reticle_daemon_start_failed_parent', {
144
- port: parsed.port,
145
- presence: settled,
146
- reason: describePresence(settled, parsed.port),
147
- log: logPath(parsed.port),
148
- stateDir: stateDirProblem(reticleStateHome()) === undefined ? 'writable' : 'unwritable',
149
- ...(startupCause === undefined ? {} : { cause: startupCause }),
150
- });
151
- // Name the CAUSE when we can see it. An unwritable state directory is a first-run failure mode
152
- // (locked-down home, read-only container mount, managed profile) that produced only "nothing is
153
- // listening" — the symptom restated — and then pointed at a log INSIDE that directory, which
154
- // cannot exist. Reproduced with `chmod 555 ~/.reticle`.
155
- const dirProblem = stateDirProblem(reticleStateHome());
156
- process.stderr.write(dirProblem === undefined
157
- ? startupCause === undefined
158
- ? `the daemon did not come up on :${String(parsed.port)} — ${describePresence(settled, parsed.port)}\n` +
159
- `see ${logPath(parsed.port)}\n`
160
- : `the daemon did not come up on :${String(parsed.port)}.\n${startupCause}\n` +
161
- `see ${logPath(parsed.port)}\n`
162
- : `the daemon did not come up on :${String(parsed.port)}.\n${dirProblem}\n`);
163
- process.exit(1);
164
- return;
165
- }
166
- log('reticle_daemon_spawned', { port: parsed.port, ...(parsed.http ? { http: true } : {}) });
97
+ process.stdout.write(`${statusLines(fields).join('\n')}\n`);
167
98
  }
168
- /** How long `serve` waits for the child to bind. Generous: a cold daemon start is seconds. */
169
- const SERVE_BIND_TIMEOUT_MS = 15_000;
170
- const PRESENCE_POLL_MS = 150;
171
- /** How long a daemon gets to shut down on its own before `stop` stops asking politely. */
172
- const GRACEFUL_STOP_MS = 5000;
173
- /** How long after SIGKILL before we accept the process is not ours to kill and say so. */
174
- const FORCED_STOP_MS = 2000;
175
- async function waitForPresence(port, want, timeoutMs) {
176
- const deadline = Date.now() + timeoutMs;
177
- for (;;) {
178
- const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
179
- if (presence === want)
180
- return true;
181
- if (Date.now() >= deadline)
182
- return false;
183
- await new Promise((resolve) => setTimeout(resolve, PRESENCE_POLL_MS));
184
- }
185
- }
186
- function handleStop(port, quiet) {
187
- const pid = readPid(port);
188
- if (null === pid || !isAlive(pid)) {
189
- removePid(port);
190
- if (!quiet)
191
- log('reticle_daemon_not_running', { port });
192
- return;
193
- }
194
- process.kill(pid, 'SIGTERM');
195
- const started = Date.now();
196
- let escalated = false;
197
- const poll = setInterval(() => {
198
- if (!isAlive(pid)) {
199
- clearInterval(poll);
200
- removePid(port);
201
- if (!quiet)
202
- log('reticle_daemon_stopped', { port, pid, ...(escalated ? { escalated } : {}) });
203
- return;
204
- }
205
- const waited = Date.now() - started;
206
- // A daemon that ignores SIGTERM is WEDGED — which is the one situation `stop` exists for. Giving
207
- // up here left the port held, the pid file stale, and a human running `kill -9` by hand at the
208
- // first two minutes of setup. Escalate instead: graceful first, then unconditional.
209
- if (!escalated && waited > GRACEFUL_STOP_MS) {
210
- escalated = true;
211
- try {
212
- process.kill(pid, 'SIGKILL');
213
- }
214
- catch {
215
- // Already gone between the liveness check and here — the next tick reports it stopped.
216
- }
217
- return;
218
- }
219
- if (waited > GRACEFUL_STOP_MS + FORCED_STOP_MS) {
220
- clearInterval(poll);
221
- // Surviving SIGKILL means the pid is not ours to kill (permissions) or is an unkillable
222
- // zombie. Neither is retryable, and both need the pid named so a human can act on it.
223
- if (!quiet)
224
- log('reticle_daemon_stop_timeout', { port, pid, escalated });
225
- process.exit(1);
226
- }
227
- }, 100);
228
- }
229
- /**
230
- * `reticle restart` — free the port, then put a daemon back on it and prove it bound.
231
- *
232
- * The two halves are reported separately on purpose. A restart that killed the old daemon and then
233
- * could not start a new one leaves the user in a different place from one that could not kill
234
- * anything at all, and a single `ok: false` cannot tell them which happened.
235
- */
236
- async function handleRestart(port, force) {
237
- const freed = await runKill(port, force);
238
- if (!freed) {
239
- log('reticle_restart_aborted', {
240
- port,
241
- reason: 'the port was not freed, so nothing was started — the old holder is still there',
242
- });
243
- process.exit(1);
244
- return;
245
- }
246
- // Reuses `serve`'s path, which waits for the daemon to ANSWER before claiming it started and exits
247
- // non-zero when it does not. A restart that reports success for a daemon that never bound is the
248
- // same lie `serve` already stopped telling.
249
- await serveWithHonestExit({ port, headless: true, http: false });
250
- }
251
- export async function handleStatus(port) {
99
+ export async function handleStatus(port, json = false) {
100
+ const report = (fields) => reportStatus(fields, json);
252
101
  const pid = readPid(port);
253
102
  // Durable, so it survives the daemon idling out — which is the state `status` is most often run in.
254
103
  const projectId = readProjectId(process.cwd());
@@ -281,7 +130,7 @@ export async function handleStatus(port) {
281
130
  // process exists; a Reticle daemon is running only when the shared port probe reaches /status.
282
131
  const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
283
132
  if (!presenceIsUsable(presence)) {
284
- log('reticle_status', {
133
+ report({
285
134
  port,
286
135
  running: false,
287
136
  presence,
@@ -310,7 +159,7 @@ export async function handleStatus(port) {
310
159
  const update = availableUpdate();
311
160
  const nudge = update === undefined ? {} : { updateAvailable: update };
312
161
  if (payload === undefined) {
313
- log('reticle_status', {
162
+ report({
314
163
  port,
315
164
  running: true,
316
165
  pid,
@@ -342,7 +191,7 @@ export async function handleStatus(port) {
342
191
  devServerPorts,
343
192
  })
344
193
  : {};
345
- log('reticle_status', {
194
+ report({
346
195
  port,
347
196
  running: true,
348
197
  pid,
@@ -752,7 +601,7 @@ export function main() {
752
601
  void handleRestart(parsed.port, parsed.force);
753
602
  break;
754
603
  case 'status':
755
- void handleStatus(parsed.port);
604
+ void handleStatus(parsed.port, parsed.json);
756
605
  break;
757
606
  case 'license':
758
607
  handleLicense();
@@ -822,6 +671,9 @@ export function main() {
822
671
  case 'gate':
823
672
  void handleGate(parsed.files, parsed.since, parsed.hook);
824
673
  break;
674
+ case 'report':
675
+ void handleReport(parsed.session, parsed.hook);
676
+ break;
825
677
  case 'watch':
826
678
  handleWatch();
827
679
  break;