@reticlehq/server 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (472) hide show
  1. package/SKILL.md +719 -0
  2. package/dist/bridge/auth-failure-reason.d.ts +19 -0
  3. package/dist/bridge/auth-failure-reason.js +29 -0
  4. package/dist/{bridge-security.d.ts → bridge/bridge-security.d.ts} +1 -1
  5. package/dist/{bridge-security.js → bridge/bridge-security.js} +1 -1
  6. package/dist/{bridge.d.ts → bridge/bridge.d.ts} +13 -1
  7. package/dist/{bridge.js → bridge/bridge.js} +95 -15
  8. package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +9 -11
  9. package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +18 -10
  10. package/dist/{pairing-token.js → bridge/pairing-token.js} +1 -1
  11. package/dist/capsule/capsule-store.d.ts +8 -3
  12. package/dist/capsule/capsule-store.js +7 -2
  13. package/dist/capsule/capsule.js +3 -3
  14. package/dist/capsule/causal-summary.d.ts +8 -0
  15. package/dist/capsule/causal-summary.js +22 -12
  16. package/dist/capsule/divergence.d.ts +9 -4
  17. package/dist/capsule/divergence.js +8 -8
  18. package/dist/capsule/predicate-to-links.js +11 -8
  19. package/dist/cli/cli-doctor.d.ts +1 -0
  20. package/dist/cli/cli-doctor.js +135 -0
  21. package/dist/{cli-flow-commands.d.ts → cli/cli-flow-commands.d.ts} +2 -2
  22. package/dist/{cli-flow-commands.js → cli/cli-flow-commands.js} +15 -15
  23. package/dist/{cli-launch.d.ts → cli/cli-launch.d.ts} +32 -3
  24. package/dist/cli/cli-launch.js +171 -0
  25. package/dist/cli/cli-parse-feedback.d.ts +31 -0
  26. package/dist/cli/cli-parse-feedback.js +56 -0
  27. package/dist/{cli-parse.d.ts → cli/cli-parse.d.ts} +6 -19
  28. package/dist/{cli-parse.js → cli/cli-parse.js} +118 -81
  29. package/dist/{cli-port.d.ts → cli/cli-port.d.ts} +12 -0
  30. package/dist/{cli-port.js → cli/cli-port.js} +9 -9
  31. package/dist/cli/cli-update-commands.d.ts +4 -0
  32. package/dist/cli/cli-update-commands.js +93 -0
  33. package/dist/{cli-verify.js → cli/cli-verify.js} +14 -14
  34. package/dist/{cloud-cli.js → cli/cloud-cli.js} +29 -24
  35. package/dist/cli/doctor-daemon-line.d.ts +37 -0
  36. package/dist/cli/doctor-daemon-line.js +35 -0
  37. package/dist/cli/port-holder.d.ts +43 -0
  38. package/dist/cli/port-holder.js +99 -0
  39. package/dist/cli.d.ts +2 -2
  40. package/dist/cli.js +302 -100
  41. package/dist/cloud/cloud-config.d.ts +0 -1
  42. package/dist/cloud/cloud-config.js +9 -9
  43. package/dist/cloud/cloud-sync.d.ts +1 -7
  44. package/dist/cloud/cloud-sync.js +12 -12
  45. package/dist/crawl/crawl-empty.d.ts +16 -0
  46. package/dist/crawl/crawl-empty.js +24 -0
  47. package/dist/crawl/crawl-tools.js +4 -1
  48. package/dist/crawl/crawl.js +26 -7
  49. package/dist/daemon/daemon-resilience.d.ts +73 -0
  50. package/dist/daemon/daemon-resilience.js +279 -0
  51. package/dist/daemon/daemon-usefulness.d.ts +61 -0
  52. package/dist/daemon/daemon-usefulness.js +62 -0
  53. package/dist/{daemon.d.ts → daemon/daemon.d.ts} +33 -8
  54. package/dist/{daemon.js → daemon/daemon.js} +105 -48
  55. package/dist/daemon/heartbeat.d.ts +115 -0
  56. package/dist/daemon/heartbeat.js +141 -0
  57. package/dist/daemon/idle-grace.d.ts +32 -0
  58. package/dist/daemon/idle-grace.js +41 -0
  59. package/dist/{idle-shutdown.d.ts → daemon/idle-shutdown.d.ts} +23 -2
  60. package/dist/{idle-shutdown.js → daemon/idle-shutdown.js} +37 -6
  61. package/dist/daemon/port-presence.d.ts +76 -0
  62. package/dist/daemon/port-presence.js +93 -0
  63. package/dist/daemon/state-dir.d.ts +7 -0
  64. package/dist/daemon/state-dir.js +59 -0
  65. package/dist/daemon/unclean-predecessor.d.ts +23 -0
  66. package/dist/daemon/unclean-predecessor.js +27 -0
  67. package/dist/domain/domain-model.js +2 -2
  68. package/dist/events/body-failures.js +8 -8
  69. package/dist/events/contradictions.d.ts +1 -1
  70. package/dist/events/contradictions.js +60 -10
  71. package/dist/events/echo-mismatch.js +10 -10
  72. package/dist/events/event-filters.js +6 -6
  73. package/dist/events/json-salvage.d.ts +2 -1
  74. package/dist/events/json-salvage.js +6 -6
  75. package/dist/events/observed-in-window.d.ts +12 -0
  76. package/dist/events/observed-in-window.js +21 -0
  77. package/dist/events/predicate-eval.d.ts +53 -20
  78. package/dist/events/predicate-eval.js +179 -55
  79. package/dist/events/predicate-parse.d.ts +23 -0
  80. package/dist/events/predicate-parse.js +54 -0
  81. package/dist/events/predicate.d.ts +7 -2
  82. package/dist/events/predicate.js +126 -35
  83. package/dist/events/reconcile.js +5 -5
  84. package/dist/events/ring-buffer.js +2 -17
  85. package/dist/events/stale-response.js +6 -6
  86. package/dist/events/testid-near-miss.d.ts +22 -0
  87. package/dist/events/testid-near-miss.js +33 -0
  88. package/dist/events/unit-mismatch.js +8 -8
  89. package/dist/flows/affected-tools.js +19 -4
  90. package/dist/flows/affected.js +1 -1
  91. package/dist/flows/annotate-target.d.ts +1 -0
  92. package/dist/flows/annotate-target.js +20 -0
  93. package/dist/flows/annotate-tools.js +25 -3
  94. package/dist/flows/annotate.js +17 -4
  95. package/dist/flows/assertion-integrity.d.ts +2 -1
  96. package/dist/flows/assertion-tiers-store.d.ts +3 -66
  97. package/dist/flows/assertion-tiers-store.js +2 -2
  98. package/dist/flows/attributed-failure.d.ts +17 -0
  99. package/dist/flows/attributed-failure.js +20 -0
  100. package/dist/flows/buddy-status.d.ts +2 -1
  101. package/dist/flows/buddy-status.js +2 -2
  102. package/dist/flows/coverage.d.ts +3 -2
  103. package/dist/flows/coverage.js +2 -2
  104. package/dist/flows/decision.js +24 -9
  105. package/dist/flows/empty-flow.d.ts +6 -0
  106. package/dist/flows/empty-flow.js +24 -0
  107. package/dist/flows/flake-store.d.ts +0 -1
  108. package/dist/flows/flake-store.js +1 -1
  109. package/dist/flows/flake.d.ts +2 -1
  110. package/dist/flows/flake.js +2 -2
  111. package/dist/flows/flow-classify.js +12 -0
  112. package/dist/flows/flow-replay-run.js +4 -4
  113. package/dist/flows/flow-replay.d.ts +6 -8
  114. package/dist/flows/flow-replay.js +132 -48
  115. package/dist/flows/flow-report.js +1 -1
  116. package/dist/flows/flow-scope.d.ts +1 -6
  117. package/dist/flows/flow-step-runners.d.ts +26 -6
  118. package/dist/flows/flow-step-runners.js +156 -6
  119. package/dist/flows/flow-success.js +20 -15
  120. package/dist/flows/flow-tools.js +63 -21
  121. package/dist/flows/flows.js +15 -11
  122. package/dist/flows/gate.d.ts +4 -3
  123. package/dist/flows/gate.js +1 -1
  124. package/dist/flows/heal-run.d.ts +0 -8
  125. package/dist/flows/heal-run.js +5 -5
  126. package/dist/flows/heal.js +3 -3
  127. package/dist/flows/predicate-to-expect.d.ts +38 -0
  128. package/dist/flows/predicate-to-expect.js +95 -0
  129. package/dist/flows/replay.d.ts +16 -1
  130. package/dist/flows/replay.js +58 -22
  131. package/dist/flows/role-anchor-nearest.d.ts +6 -0
  132. package/dist/flows/role-anchor-nearest.js +45 -0
  133. package/dist/flows/role-drift-reason.d.ts +20 -0
  134. package/dist/flows/role-drift-reason.js +39 -0
  135. package/dist/flows/server-verify.d.ts +1 -1
  136. package/dist/flows/server-verify.js +5 -5
  137. package/dist/flows/suite-flakes.d.ts +33 -0
  138. package/dist/flows/suite-flakes.js +32 -0
  139. package/dist/flows/verify-change-tools.js +32 -10
  140. package/dist/flows/verify-change-verdict.d.ts +15 -0
  141. package/dist/flows/verify-change-verdict.js +25 -0
  142. package/dist/flows/watch-batcher.d.ts +3 -2
  143. package/dist/honesty/already-true.d.ts +2 -0
  144. package/dist/honesty/already-true.js +31 -0
  145. package/dist/honesty/blind-spots.d.ts +35 -3
  146. package/dist/honesty/blind-spots.js +57 -12
  147. package/dist/honesty/honesty.d.ts +3 -2
  148. package/dist/honesty/honesty.js +4 -4
  149. package/dist/honesty/uncaptured-bodies.js +1 -1
  150. package/dist/honesty/unread-outcome.js +2 -2
  151. package/dist/honesty/verified.d.ts +31 -3
  152. package/dist/honesty/verified.js +64 -8
  153. package/dist/http-server.js +23 -7
  154. package/dist/hunt/hunt-report.d.ts +2 -1
  155. package/dist/hunt/hunt-report.js +2 -2
  156. package/dist/index.d.ts +9 -6
  157. package/dist/index.js +41 -25
  158. package/dist/init/agent-rules.d.ts +20 -1
  159. package/dist/init/agent-rules.js +64 -8
  160. package/dist/init/app-choice.d.ts +20 -0
  161. package/dist/init/app-choice.js +25 -0
  162. package/dist/init/astro-host.d.ts +21 -0
  163. package/dist/init/astro-host.js +27 -0
  164. package/dist/init/astro-patch.d.ts +16 -0
  165. package/dist/init/astro-patch.js +240 -0
  166. package/dist/init/capabilities.js +4 -1
  167. package/dist/init/cra.d.ts +50 -0
  168. package/dist/init/cra.js +106 -0
  169. package/dist/init/cursor.js +39 -4
  170. package/dist/init/desktop-doctor.d.ts +3 -2
  171. package/dist/init/desktop-doctor.js +4 -4
  172. package/dist/init/detect.d.ts +9 -2
  173. package/dist/init/detect.js +24 -5
  174. package/dist/init/mcp-clients.d.ts +81 -0
  175. package/dist/init/mcp-clients.js +248 -0
  176. package/dist/init/mcp-registered.d.ts +12 -0
  177. package/dist/init/mcp-registered.js +14 -0
  178. package/dist/init/mcp.d.ts +27 -2
  179. package/dist/init/mcp.js +40 -4
  180. package/dist/init/next-patch.d.ts +2 -4
  181. package/dist/init/next-patch.js +109 -19
  182. package/dist/init/node-io.js +29 -6
  183. package/dist/init/plan-framework.d.ts +16 -1
  184. package/dist/init/plan-framework.js +166 -8
  185. package/dist/init/plan.d.ts +85 -6
  186. package/dist/init/plan.js +196 -33
  187. package/dist/init/project-id.d.ts +5 -9
  188. package/dist/init/project-id.js +10 -22
  189. package/dist/init/run.d.ts +23 -1
  190. package/dist/init/run.js +338 -47
  191. package/dist/init/slash-command.d.ts +9 -0
  192. package/dist/init/slash-command.js +10 -1
  193. package/dist/init/snippets.d.ts +7 -10
  194. package/dist/init/snippets.js +44 -15
  195. package/dist/init/vite-config.js +1 -1
  196. package/dist/init/windows-quote.d.ts +38 -0
  197. package/dist/init/windows-quote.js +70 -0
  198. package/dist/init/workspace-apps.d.ts +23 -0
  199. package/dist/init/workspace-apps.js +68 -0
  200. package/dist/input/driven-redaction.js +1 -1
  201. package/dist/input/network-detail.js +8 -8
  202. package/dist/input/network-mock-tools.js +2 -2
  203. package/dist/input/network-mock.js +4 -4
  204. package/dist/input/real-input.js +8 -3
  205. package/dist/input/scroll-find.js +2 -2
  206. package/dist/input/scroll-tools.d.ts +0 -5
  207. package/dist/input/scroll-tools.js +11 -4
  208. package/dist/input/viewport-tools.js +2 -2
  209. package/dist/journal/ambient-store.d.ts +0 -2
  210. package/dist/journal/ambient-store.js +1 -1
  211. package/dist/journal/ambient.js +1 -1
  212. package/dist/journal/attach-journal.d.ts +2 -1
  213. package/dist/journal/deviation-report.js +6 -6
  214. package/dist/journal/deviation-service.js +1 -1
  215. package/dist/journal/envelope-store.d.ts +0 -2
  216. package/dist/journal/envelope-store.js +1 -1
  217. package/dist/journal/envelope.d.ts +2 -3
  218. package/dist/journal/envelope.js +3 -3
  219. package/dist/journal/journal-query.js +1 -1
  220. package/dist/journal/journal-recorder.d.ts +2 -1
  221. package/dist/journal/journal-recorder.js +2 -2
  222. package/dist/journal/rollups.js +9 -9
  223. package/dist/journal/session-end.d.ts +2 -1
  224. package/dist/journal/session-journal.js +4 -4
  225. package/dist/license/license.d.ts +0 -14
  226. package/dist/license/license.js +8 -8
  227. package/dist/log.d.ts +10 -0
  228. package/dist/log.js +11 -1
  229. package/dist/mcp/mcp-is-error.d.ts +13 -0
  230. package/dist/mcp/mcp-is-error.js +18 -0
  231. package/dist/mcp/mcp-outage.d.ts +32 -0
  232. package/dist/mcp/mcp-outage.js +55 -0
  233. package/dist/mcp/mcp-proxy.d.ts +134 -0
  234. package/dist/mcp/mcp-proxy.js +720 -0
  235. package/dist/{mcp.d.ts → mcp/mcp.d.ts} +13 -10
  236. package/dist/{mcp.js → mcp/mcp.js} +139 -37
  237. package/dist/mcp/proxy-handshake.d.ts +3 -0
  238. package/dist/mcp/proxy-handshake.js +54 -0
  239. package/dist/mcp/proxy-lifecycle.d.ts +64 -0
  240. package/dist/mcp/proxy-lifecycle.js +70 -0
  241. package/dist/mcp/tool-catalog-cache.d.ts +29 -0
  242. package/dist/mcp/tool-catalog-cache.js +55 -0
  243. package/dist/oracles/propose-consequences.d.ts +2 -1
  244. package/dist/oracles/propose-consequences.js +16 -11
  245. package/dist/oracles/self-instrument.d.ts +2 -1
  246. package/dist/oracles/self-instrument.js +1 -1
  247. package/dist/phenomena/phenomena.js +3 -3
  248. package/dist/platform.d.ts +16 -0
  249. package/dist/platform.js +16 -0
  250. package/dist/pool/browser-pool.d.ts +9 -0
  251. package/dist/pool/browser-pool.js +38 -9
  252. package/dist/pool/playwright-launcher.d.ts +15 -2
  253. package/dist/pool/playwright-launcher.js +21 -2
  254. package/dist/project/fs-port.js +1 -1
  255. package/dist/project/project-tools.js +2 -2
  256. package/dist/runs/build-verification-run.js +1 -1
  257. package/dist/runs/repair-prompt.d.ts +0 -6
  258. package/dist/runs/repair-prompt.js +3 -3
  259. package/dist/runs/risk-classify.js +1 -1
  260. package/dist/runs/run-diff.d.ts +0 -2
  261. package/dist/runs/run-diff.js +3 -3
  262. package/dist/runs/run-tools.js +2 -2
  263. package/dist/runs/verification-sync.js +2 -2
  264. package/dist/runs/verify-http.js +1 -1
  265. package/dist/runs/verify-server.js +1 -1
  266. package/dist/session/attachment-history.d.ts +46 -0
  267. package/dist/session/attachment-history.js +65 -0
  268. package/dist/session/command-timeout.d.ts +11 -3
  269. package/dist/session/command-timeout.js +2 -2
  270. package/dist/session/control-envelope.d.ts +6 -0
  271. package/dist/session/control-envelope.js +7 -1
  272. package/dist/session/dev-server-probe.d.ts +13 -0
  273. package/dist/session/dev-server-probe.js +52 -0
  274. package/dist/session/last-act.d.ts +17 -3
  275. package/dist/session/last-act.js +16 -4
  276. package/dist/session/live-control-tools.js +7 -2
  277. package/dist/session/live-control.js +1 -1
  278. package/dist/session/looks-like-dev-server.d.ts +14 -0
  279. package/dist/session/looks-like-dev-server.js +25 -0
  280. package/dist/session/no-session-diagnosis.d.ts +45 -0
  281. package/dist/session/no-session-diagnosis.js +120 -0
  282. package/dist/session/no-session-watch.d.ts +21 -0
  283. package/dist/session/no-session-watch.js +69 -0
  284. package/dist/session/observed-state.d.ts +4 -0
  285. package/dist/session/observed-state.js +19 -1
  286. package/dist/session/output-budget.js +1 -1
  287. package/dist/session/pending-commands.js +1 -0
  288. package/dist/session/session-health.d.ts +8 -1
  289. package/dist/session/session-health.js +8 -7
  290. package/dist/session/session-info.d.ts +43 -8
  291. package/dist/session/session-info.js +46 -1
  292. package/dist/session/session-lease.d.ts +0 -11
  293. package/dist/session/session-lease.js +1 -1
  294. package/dist/session/session-manager.d.ts +6 -0
  295. package/dist/session/session-manager.js +124 -12
  296. package/dist/session/session-readiness.d.ts +1 -1
  297. package/dist/session/session-readiness.js +6 -1
  298. package/dist/session/session-reaper.js +1 -1
  299. package/dist/session/session-reconnect.d.ts +32 -0
  300. package/dist/session/session-reconnect.js +42 -0
  301. package/dist/session/session.d.ts +15 -2
  302. package/dist/session/session.js +77 -26
  303. package/dist/session/state-select.d.ts +1 -1
  304. package/dist/session/state-select.js +1 -1
  305. package/dist/status-payload.d.ts +19 -0
  306. package/dist/status-payload.js +11 -0
  307. package/dist/telemetry/argument-shape.js +10 -1
  308. package/dist/telemetry/browser-mode.d.ts +27 -0
  309. package/dist/telemetry/browser-mode.js +33 -0
  310. package/dist/telemetry/bug-found.d.ts +17 -21
  311. package/dist/telemetry/bug-found.js +120 -8
  312. package/dist/telemetry/cli-telemetry.d.ts +2 -12
  313. package/dist/telemetry/cli-telemetry.js +27 -4
  314. package/dist/telemetry/crash-cause.d.ts +47 -0
  315. package/dist/telemetry/crash-cause.js +104 -0
  316. package/dist/telemetry/daemon-telemetry.d.ts +38 -1
  317. package/dist/telemetry/daemon-telemetry.js +37 -12
  318. package/dist/telemetry/dev-repo.js +4 -4
  319. package/dist/telemetry/error-class.d.ts +26 -0
  320. package/dist/telemetry/error-class.js +52 -0
  321. package/dist/telemetry/error-fingerprint.js +21 -2
  322. package/dist/telemetry/feedback-cli.d.ts +8 -3
  323. package/dist/telemetry/feedback-cli.js +52 -8
  324. package/dist/telemetry/feedback-context.d.ts +23 -6
  325. package/dist/telemetry/feedback-context.js +105 -5
  326. package/dist/telemetry/feedback-delivery.d.ts +20 -0
  327. package/dist/telemetry/feedback-delivery.js +30 -0
  328. package/dist/telemetry/feedback-outbox.d.ts +22 -0
  329. package/dist/telemetry/feedback-outbox.js +87 -0
  330. package/dist/telemetry/feedback.d.ts +22 -1
  331. package/dist/telemetry/feedback.js +67 -8
  332. package/dist/telemetry/git-facts.d.ts +7 -0
  333. package/dist/telemetry/git-facts.js +5 -5
  334. package/dist/telemetry/project-profile.js +2 -1
  335. package/dist/telemetry/session-metrics.d.ts +47 -3
  336. package/dist/telemetry/session-metrics.js +315 -15
  337. package/dist/telemetry/telemetry.d.ts +45 -2
  338. package/dist/telemetry/telemetry.js +150 -37
  339. package/dist/telemetry/verification-of.d.ts +22 -0
  340. package/dist/telemetry/verification-of.js +84 -0
  341. package/dist/tools/act-capsule.js +2 -2
  342. package/dist/tools/act-danger.d.ts +12 -0
  343. package/dist/tools/act-danger.js +20 -3
  344. package/dist/tools/act-helpers.js +4 -4
  345. package/dist/tools/act-tools.d.ts +0 -8
  346. package/dist/tools/act-tools.js +114 -118
  347. package/dist/tools/act-view.js +7 -7
  348. package/dist/tools/alias-args.d.ts +17 -0
  349. package/dist/tools/alias-args.js +25 -0
  350. package/dist/tools/assert-grade.js +9 -9
  351. package/dist/tools/assert-verdict.d.ts +9 -1
  352. package/dist/tools/assert-verdict.js +21 -6
  353. package/dist/tools/browser-tools.js +36 -11
  354. package/dist/tools/contract-tools.d.ts +0 -5
  355. package/dist/tools/contract-tools.js +27 -1
  356. package/dist/tools/coverage-identity.d.ts +34 -0
  357. package/dist/tools/coverage-identity.js +46 -0
  358. package/dist/tools/coverage-tools.js +10 -14
  359. package/dist/tools/dynamic-tools.d.ts +2 -1
  360. package/dist/tools/dynamic-tools.js +96 -19
  361. package/dist/tools/error-recovery.d.ts +34 -2
  362. package/dist/tools/error-recovery.js +255 -4
  363. package/dist/tools/feedback-invite.d.ts +48 -0
  364. package/dist/tools/feedback-invite.js +71 -0
  365. package/dist/tools/feedback-tools.d.ts +12 -1
  366. package/dist/tools/feedback-tools.js +28 -5
  367. package/dist/tools/invoke-tool.d.ts +0 -12
  368. package/dist/tools/invoke-tool.js +126 -33
  369. package/dist/tools/lease-hint.d.ts +17 -0
  370. package/dist/tools/lease-hint.js +24 -0
  371. package/dist/tools/lease-tools.js +6 -5
  372. package/dist/tools/merge-tools.d.ts +10 -1
  373. package/dist/tools/merge-tools.js +9 -3
  374. package/dist/tools/merged-name-redirect.d.ts +13 -0
  375. package/dist/tools/merged-name-redirect.js +55 -0
  376. package/dist/tools/navigate-arrival.d.ts +26 -0
  377. package/dist/tools/navigate-arrival.js +60 -0
  378. package/dist/tools/navigate-result.d.ts +31 -0
  379. package/dist/tools/navigate-result.js +38 -0
  380. package/dist/tools/observe-tools.js +72 -20
  381. package/dist/tools/observed-nothing.d.ts +25 -0
  382. package/dist/tools/observed-nothing.js +33 -0
  383. package/dist/tools/query-paginate.js +2 -2
  384. package/dist/tools/query-shape.d.ts +18 -0
  385. package/dist/tools/query-shape.js +39 -0
  386. package/dist/tools/read-tools.js +81 -20
  387. package/dist/tools/real-input-attempt.d.ts +28 -0
  388. package/dist/tools/real-input-attempt.js +93 -0
  389. package/dist/tools/reconcile-tools.js +7 -7
  390. package/dist/tools/reload-result.d.ts +14 -0
  391. package/dist/tools/reload-result.js +29 -0
  392. package/dist/tools/snapshot-delta.d.ts +4 -5
  393. package/dist/tools/snapshot-delta.js +0 -0
  394. package/dist/tools/tool-kit.d.ts +16 -0
  395. package/dist/tools/tool-surface.d.ts +83 -0
  396. package/dist/tools/tool-surface.js +217 -0
  397. package/dist/tools/tools-helpers.js +6 -6
  398. package/dist/tools/tools.d.ts +15 -0
  399. package/dist/tools/tools.js +53 -17
  400. package/dist/tools/unadvertised-help.d.ts +5 -0
  401. package/dist/tools/unadvertised-help.js +39 -0
  402. package/dist/trace.d.ts +69 -0
  403. package/dist/trace.js +150 -0
  404. package/dist/update/nudge-credit.d.ts +4 -0
  405. package/dist/update/nudge-credit.js +53 -0
  406. package/dist/update/sdk-sync.d.ts +21 -0
  407. package/dist/update/sdk-sync.js +42 -0
  408. package/dist/update/update-checker.js +3 -3
  409. package/dist/update/update-nudge.d.ts +63 -6
  410. package/dist/update/update-nudge.js +128 -15
  411. package/dist/update/updater.d.ts +1 -9
  412. package/dist/update/updater.js +13 -8
  413. package/dist/version/peer-announce.d.ts +4 -0
  414. package/dist/version/peer-announce.js +30 -0
  415. package/dist/{server-version.js → version/server-version.js} +5 -1
  416. package/dist/version/version-nudge.d.ts +28 -0
  417. package/dist/version/version-nudge.js +40 -0
  418. package/dist/version/version-skew.d.ts +66 -0
  419. package/dist/version/version-skew.js +71 -0
  420. package/dist/visual/visual-diff.js +2 -2
  421. package/dist/visual/visual-tools.js +4 -4
  422. package/docs/README.md +55 -0
  423. package/docs/agent-cheatsheet.md +85 -0
  424. package/docs/architecture.md +113 -0
  425. package/docs/benchmarks.md +255 -0
  426. package/docs/debugging.md +102 -0
  427. package/docs/deploy-checks.md +69 -0
  428. package/docs/desktop-apps.md +256 -0
  429. package/docs/enterprise.md +62 -0
  430. package/docs/fixtures-dispatch-receiver.yml +76 -0
  431. package/docs/fixtures.md +45 -0
  432. package/docs/flows.md +166 -0
  433. package/docs/gate-plan.md +98 -0
  434. package/docs/gates.md +117 -0
  435. package/docs/getting-started.md +469 -0
  436. package/docs/human-control.md +63 -0
  437. package/docs/integration-patterns.md +171 -0
  438. package/docs/local-registry.md +126 -0
  439. package/docs/matrix/2.5.0/claude-code-machine.json +19 -0
  440. package/docs/matrix/2.5.0/codex-machine.json +20 -0
  441. package/docs/matrix/2.5.0/cursor-machine.json +22 -0
  442. package/docs/matrix/2.5.0/gemini-machine.json +22 -0
  443. package/docs/matrix/2.5.0/opencode-machine.json +22 -0
  444. package/docs/matrix/2.5.0/vscode-machine.json +22 -0
  445. package/docs/matrix/2.5.0/windsurf-machine.json +22 -0
  446. package/docs/matrix/MATRIX.md +26 -0
  447. package/docs/matrix/README.md +59 -0
  448. package/docs/mint.json +61 -0
  449. package/docs/multi-agent-testing.md +55 -0
  450. package/docs/platform-integration.md +122 -0
  451. package/docs/system-map.md +152 -0
  452. package/docs/telemetry-contract.md +214 -0
  453. package/docs/telemetry.md +191 -0
  454. package/docs/testing.md +82 -0
  455. package/docs/token-efficiency.md +80 -0
  456. package/docs/usage.md +1046 -0
  457. package/package.json +8 -5
  458. package/dist/cli-doctor.d.ts +0 -6
  459. package/dist/cli-doctor.js +0 -60
  460. package/dist/cli-launch.js +0 -112
  461. package/dist/daemon-resilience.d.ts +0 -22
  462. package/dist/daemon-resilience.js +0 -85
  463. package/dist/mcp-proxy.d.ts +0 -67
  464. package/dist/mcp-proxy.js +0 -337
  465. package/dist/tools/profiles.d.ts +0 -37
  466. package/dist/tools/profiles.js +0 -139
  467. /package/dist/{pairing-token.d.ts → bridge/pairing-token.d.ts} +0 -0
  468. /package/dist/{token-auth.d.ts → bridge/token-auth.d.ts} +0 -0
  469. /package/dist/{token-auth.js → bridge/token-auth.js} +0 -0
  470. /package/dist/{cli-verify.d.ts → cli/cli-verify.d.ts} +0 -0
  471. /package/dist/{cloud-cli.d.ts → cli/cloud-cli.d.ts} +0 -0
  472. /package/dist/{server-version.d.ts → version/server-version.d.ts} +0 -0
@@ -1,15 +1,10 @@
1
1
  import { McpScope, type Feedback } from '@reticlehq/core';
2
2
  /** The context fields — the whole `Feedback` shape minus what the author supplies. */
3
3
  export type FeedbackContext = Pick<Feedback, 'stack' | 'stackMajor' | 'runtime' | 'engine' | 'driver' | 'client' | 'clientVersion' | 'mcpScope'>;
4
- /**
5
- * The project's framework and its MAJOR version, or `{}` when package.json is absent, unreadable, or
6
- * names nothing we recognize. The major comes from `parseMajor` — the same range parser `reticle init`
7
- * uses to decide React 19 source mapping — so a range like `^15.0.0-canary.3` narrows the same way in
8
- * both places instead of via a second, subtly different reader.
9
- */
10
4
  export declare function detectStack(cwd: string, read?: (path: string) => string): {
11
5
  stack?: string;
12
6
  stackMajor?: number;
7
+ stackSource?: 'cwd' | 'workspace';
13
8
  };
14
9
  /**
15
10
  * User scope unless the project checks in its own MCP registration. `reticle init` deliberately
@@ -27,6 +22,28 @@ export interface SessionFacts {
27
22
  engine?: string;
28
23
  /** True when Reticle drives the page over CDP rather than observing the human's own browser. */
29
24
  driven?: boolean;
25
+ /** What the APP said it is running, from the SDK's hello. The only session-scoped stack evidence. */
26
+ adapters?: string[];
30
27
  }
28
+ /**
29
+ * Reconcile what the DIRECTORY says with what the APP says.
30
+ *
31
+ * `detectStack` reads the daemon's cwd, which is not the session's project whenever one daemon
32
+ * serves several apps — the normal case for anyone with two dev servers, and the default in the
33
+ * fixtures. Reported from the field: feedback filed from an astro session arrived stamped
34
+ * `"stack":"sveltekit"`, because that was the directory the daemon happened to be started in. The
35
+ * sessionId was passed and ignored.
36
+ *
37
+ * The rule: keep the directory's answer only while the app agrees with it. An adapter that implies a
38
+ * different family means the directory is describing somebody else's project, so the app's own
39
+ * report wins — less specific, and true, which is the right trade for an attribution field.
40
+ */
41
+ export declare function reconcileStack(fromCwd: {
42
+ stack?: string;
43
+ stackMajor?: number;
44
+ }, adapters: readonly string[] | undefined): {
45
+ stack?: string;
46
+ stackMajor?: number;
47
+ };
31
48
  /** Assemble the full context. Every field independently optional — a partial context still ships. */
32
49
  export declare function feedbackContext(cwd: string, session?: SessionFacts): FeedbackContext;
@@ -9,10 +9,11 @@
9
9
  * holds to. Detection is entirely best-effort: an unreadable package.json or an SDK too old to report
10
10
  * its runtime yields `undefined`, never a throw and never a guess.
11
11
  */
12
- import { readFileSync } from 'node:fs';
13
- import { join } from 'node:path';
12
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
13
+ import { isAbsolute, join } from 'node:path';
14
14
  import { AppRuntime, McpScope, PageDriver } from '@reticlehq/core';
15
15
  import { parseMajor } from '../init/detect.js';
16
+ import { findWorkspaceApps } from '../init/run.js';
16
17
  /**
17
18
  * Dependency → reported stack, in resolution order. Meta-frameworks come FIRST: a Next app also
18
19
  * depends on react, and reporting `react` for it would collapse the single most important segment
@@ -44,10 +45,10 @@ const PROJECT_MCP_FILES = ['.mcp.json', join('.cursor', 'mcp.json'), join('.vsco
44
45
  * uses to decide React 19 source mapping — so a range like `^15.0.0-canary.3` narrows the same way in
45
46
  * both places instead of via a second, subtly different reader.
46
47
  */
47
- export function detectStack(cwd, read = readFile) {
48
+ function stackOfManifest(dir, read) {
48
49
  let pkg;
49
50
  try {
50
- pkg = JSON.parse(read(join(cwd, PACKAGE_JSON)));
51
+ pkg = JSON.parse(read(join(dir, PACKAGE_JSON)));
51
52
  }
52
53
  catch {
53
54
  return {};
@@ -62,6 +63,65 @@ export function detectStack(cwd, read = readFile) {
62
63
  }
63
64
  return {};
64
65
  }
66
+ export function detectStack(cwd, read = readFile) {
67
+ // The cwd wins when it IS an app — never redirect away from the real answer.
68
+ const here = stackOfManifest(cwd, read);
69
+ if (here.stack !== undefined)
70
+ return { ...here, stackSource: 'cwd' };
71
+ // Otherwise look for the app. The daemon's cwd is wherever the agent's client happened to launch,
72
+ // which for a real repo is the ROOT while the app sits in `frontend/`, `web/`, or a declared
73
+ // workspace. Reading one directory and giving up is why this returned nothing for every project
74
+ // that actually had Reticle set up: 0 of 77 instrumented projects, and 0 of 166 `size: huge` ones.
75
+ // The bigger the repo, the more certainly we failed.
76
+ //
77
+ // `findWorkspaceApps` is init's discovery, reused rather than reimplemented: it already reads
78
+ // declared workspaces (pnpm-workspace.yaml, package.json `workspaces`) AND scans top-level
79
+ // directories, and it was widened once already for a repo shape a user hit twice. A second
80
+ // detector here would drift from it the first time either changed.
81
+ try {
82
+ for (const app of findWorkspaceApps(profileIo(cwd))) {
83
+ const found = stackOfManifest(join(cwd, app), read);
84
+ // Reported as `workspace` so the share of projects needing discovery is measurable — that
85
+ // number is what says whether our inference needs an agent to correct it.
86
+ if (found.stack !== undefined)
87
+ return { ...found, stackSource: 'workspace' };
88
+ }
89
+ }
90
+ catch {
91
+ // Discovery touches the filesystem; a permission error must never cost us the profile event.
92
+ }
93
+ return {};
94
+ }
95
+ /**
96
+ * The three filesystem methods `findWorkspaceApps` needs, rooted at `cwd`.
97
+ *
98
+ * Deliberately not the whole `InitIo`: this path must not be able to write, exec or print. What it
99
+ * cannot do is a stronger guarantee than a rule saying it shouldn't.
100
+ */
101
+ function profileIo(cwd) {
102
+ const abs = (rel) => (isAbsolute(rel) ? rel : join(cwd, rel));
103
+ return {
104
+ readFile: (rel) => {
105
+ try {
106
+ return readFileSync(abs(rel), 'utf8');
107
+ }
108
+ catch {
109
+ return null;
110
+ }
111
+ },
112
+ exists: (rel) => existsSync(abs(rel)),
113
+ listDirs: (rel) => {
114
+ try {
115
+ return readdirSync(abs(rel), { withFileTypes: true })
116
+ .filter((e) => e.isDirectory() && !e.name.startsWith('.') && 'node_modules' !== e.name)
117
+ .map((e) => e.name);
118
+ }
119
+ catch {
120
+ return [];
121
+ }
122
+ },
123
+ };
124
+ }
65
125
  /**
66
126
  * User scope unless the project checks in its own MCP registration. `reticle init` deliberately
67
127
  * registers ONCE at user scope, so `project` here means the team wired it up by hand — which is
@@ -116,6 +176,43 @@ function detectClient() {
116
176
  return {};
117
177
  }
118
178
  }
179
+ /**
180
+ * Which cwd-derived stacks each SDK adapter is consistent with.
181
+ *
182
+ * The adapter names a UI library; the directory may name a meta-framework built on it. `react` and
183
+ * `next` are the same app described at two levels, so the more specific one is kept. `react` and
184
+ * `sveltekit` are not — that pairing means the daemon's directory is not this session's project.
185
+ */
186
+ const ADAPTER_IMPLIES = {
187
+ react: ['react', 'next', 'remix'],
188
+ svelte: ['svelte', 'sveltekit'],
189
+ vue: ['vue', 'nuxt'],
190
+ solid: ['solid'],
191
+ angular: ['angular'],
192
+ };
193
+ /**
194
+ * Reconcile what the DIRECTORY says with what the APP says.
195
+ *
196
+ * `detectStack` reads the daemon's cwd, which is not the session's project whenever one daemon
197
+ * serves several apps — the normal case for anyone with two dev servers, and the default in the
198
+ * fixtures. Reported from the field: feedback filed from an astro session arrived stamped
199
+ * `"stack":"sveltekit"`, because that was the directory the daemon happened to be started in. The
200
+ * sessionId was passed and ignored.
201
+ *
202
+ * The rule: keep the directory's answer only while the app agrees with it. An adapter that implies a
203
+ * different family means the directory is describing somebody else's project, so the app's own
204
+ * report wins — less specific, and true, which is the right trade for an attribution field.
205
+ */
206
+ export function reconcileStack(fromCwd, adapters) {
207
+ const claimed = adapters?.find((adapter) => adapter in ADAPTER_IMPLIES);
208
+ if (claimed === undefined)
209
+ return fromCwd;
210
+ const consistent = ADAPTER_IMPLIES[claimed] ?? [];
211
+ if (fromCwd.stack !== undefined && consistent.includes(fromCwd.stack))
212
+ return fromCwd;
213
+ // The major belonged to the directory's framework, so it cannot travel with a different stack.
214
+ return { stack: claimed };
215
+ }
119
216
  function asRuntime(value) {
120
217
  return Object.values(AppRuntime).find((known) => known === value);
121
218
  }
@@ -131,7 +228,10 @@ export function feedbackContext(cwd, session) {
131
228
  const engine = asEngine(session?.engine);
132
229
  const client = detectClient();
133
230
  return {
134
- ...detectStack(cwd),
231
+ // Session-scoped when the app reports something that contradicts the directory — see
232
+ // reconcileStack. A daemon serving two apps otherwise stamps both with whichever one it was
233
+ // started next to.
234
+ ...reconcileStack(detectStack(cwd), session?.adapters),
135
235
  ...client,
136
236
  ...(runtime !== undefined ? { runtime } : {}),
137
237
  ...(engine !== undefined ? { engine } : {}),
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Telling the reporter that a BACKGROUND feedback delivery failed.
3
+ *
4
+ * The agent's `reticle_feedback` no longer waits for the POST — a ~340ms network round-trip on the
5
+ * one call an agent makes mid-task is the product blocking the user's work to talk about itself. But
6
+ * not waiting reopens the exact hole the awaited version was written to close: `sent` used to be
7
+ * unconditional, so a DNS miss and a 4xx both reported "filed", and this is the ONLY qualitative
8
+ * channel the product has. A lost report that was announced as filed is worse than no channel.
9
+ *
10
+ * So the wait goes and the honesty stays: the receipt says ACCEPTED (validated, redacted, queued),
11
+ * never "delivered", and if the send then fails the agent is told on its next tool result through
12
+ * the same one-shot envelope the update and skew nudges use. Told once — a reporter who cannot fix
13
+ * the network does not need it repeated, and a banner on every call is one agents learn to skip.
14
+ */
15
+ /** Record that a queued report never made it. */
16
+ export declare function noteFeedbackUndelivered(reason: string): void;
17
+ /** The undelivered-feedback notice, once. */
18
+ export declare function takeFeedbackUndelivered(): string | undefined;
19
+ /** Tests only — drop the module state so each case starts clean. */
20
+ export declare function resetFeedbackDelivery(): void;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Telling the reporter that a BACKGROUND feedback delivery failed.
3
+ *
4
+ * The agent's `reticle_feedback` no longer waits for the POST — a ~340ms network round-trip on the
5
+ * one call an agent makes mid-task is the product blocking the user's work to talk about itself. But
6
+ * not waiting reopens the exact hole the awaited version was written to close: `sent` used to be
7
+ * unconditional, so a DNS miss and a 4xx both reported "filed", and this is the ONLY qualitative
8
+ * channel the product has. A lost report that was announced as filed is worse than no channel.
9
+ *
10
+ * So the wait goes and the honesty stays: the receipt says ACCEPTED (validated, redacted, queued),
11
+ * never "delivered", and if the send then fails the agent is told on its next tool result through
12
+ * the same one-shot envelope the update and skew nudges use. Told once — a reporter who cannot fix
13
+ * the network does not need it repeated, and a banner on every call is one agents learn to skip.
14
+ */
15
+ /** Set when a background feedback send failed and the reporter has not been told yet. */
16
+ let pending;
17
+ /** Record that a queued report never made it. */
18
+ export function noteFeedbackUndelivered(reason) {
19
+ pending = reason;
20
+ }
21
+ /** The undelivered-feedback notice, once. */
22
+ export function takeFeedbackUndelivered() {
23
+ const notice = pending;
24
+ pending = undefined;
25
+ return notice;
26
+ }
27
+ /** Tests only — drop the module state so each case starts clean. */
28
+ export function resetFeedbackDelivery() {
29
+ pending = undefined;
30
+ }
@@ -0,0 +1,22 @@
1
+ /** One queued report: the payload, plus what is needed to find and remove it again. */
2
+ export interface OutboxEntry {
3
+ id: string;
4
+ t: string;
5
+ payload: unknown;
6
+ }
7
+ export declare function outboxPath(home?: string): string;
8
+ /**
9
+ * Persist a report and return its id. Never throws: failing to queue must not also fail the send
10
+ * that was about to happen anyway.
11
+ */
12
+ export declare function queueFeedback(payload: unknown, now?: () => Date, home?: string): string | null;
13
+ /** Read every queued report. A malformed line is skipped, never fatal — the rest still matter. */
14
+ export declare function readOutbox(home?: string): OutboxEntry[];
15
+ /**
16
+ * Drop a report that was delivered. Rewrites the file without it.
17
+ *
18
+ * A rewrite rather than a tombstone because the file's whole purpose is "what still needs sending" —
19
+ * a reader should be able to `cat` it and see exactly the backlog, with no filtering rule to learn.
20
+ * Volume is a handful of lines a week, so the cost is irrelevant.
21
+ */
22
+ export declare function markDelivered(id: string | null, home?: string): void;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * A durable outbox for feedback, written BEFORE the network is touched.
3
+ *
4
+ * Feedback is the only qualitative channel this product has, and it carries an agent's whole
5
+ * root-cause analysis — often an hour of driving distilled into one payload. It was sent with the
6
+ * same 2-second fire-and-forget budget as a usage counter, with no retry and no persistence, so a
7
+ * 1.3-second network hiccup destroyed the report outright. Measured to the collector from one
8
+ * machine with a WARM DNS cache: dns 0.003s, connect 0.233s, tls 0.467s, total 0.694s — roughly a
9
+ * third of the entire budget spent before a byte of payload moves, and that is the good case. A
10
+ * fresh short-lived CLI process pays cold DNS and a cold route on top.
11
+ *
12
+ * So the order is inverted: write it down first, then try to send. A failed send is then a queued
13
+ * report rather than a lost one, and `sent: false` becomes a statement about the network instead of
14
+ * an epitaph.
15
+ *
16
+ * JSONL, appended, one payload per line. Append-only is the point — a crash mid-write costs at most
17
+ * the line being written, never the ones already there.
18
+ */
19
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
20
+ import { homedir } from 'node:os';
21
+ import { join } from 'node:path';
22
+ import { randomUUID } from 'node:crypto';
23
+ import { ReticleDir } from '@reticlehq/core';
24
+ export function outboxPath(home = homedir()) {
25
+ return join(home, ReticleDir.ROOT, 'feedback-outbox.jsonl');
26
+ }
27
+ /**
28
+ * Persist a report and return its id. Never throws: failing to queue must not also fail the send
29
+ * that was about to happen anyway.
30
+ */
31
+ export function queueFeedback(payload, now = () => new Date(), home = homedir()) {
32
+ const id = randomUUID();
33
+ try {
34
+ mkdirSync(join(home, ReticleDir.ROOT), { recursive: true });
35
+ const entry = { id, t: now().toISOString(), payload };
36
+ appendFileSync(outboxPath(home), `${JSON.stringify(entry)}\n`, 'utf8');
37
+ return id;
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ /** Read every queued report. A malformed line is skipped, never fatal — the rest still matter. */
44
+ export function readOutbox(home = homedir()) {
45
+ try {
46
+ const path = outboxPath(home);
47
+ if (!existsSync(path))
48
+ return [];
49
+ return readFileSync(path, 'utf8')
50
+ .split('\n')
51
+ .filter((line) => 0 < line.trim().length)
52
+ .flatMap((line) => {
53
+ try {
54
+ return [JSON.parse(line)];
55
+ }
56
+ catch {
57
+ return [];
58
+ }
59
+ });
60
+ }
61
+ catch {
62
+ return [];
63
+ }
64
+ }
65
+ /**
66
+ * Drop a report that was delivered. Rewrites the file without it.
67
+ *
68
+ * A rewrite rather than a tombstone because the file's whole purpose is "what still needs sending" —
69
+ * a reader should be able to `cat` it and see exactly the backlog, with no filtering rule to learn.
70
+ * Volume is a handful of lines a week, so the cost is irrelevant.
71
+ */
72
+ export function markDelivered(id, home = homedir()) {
73
+ if (null === id)
74
+ return;
75
+ try {
76
+ const remaining = readOutbox(home).filter((entry) => entry.id !== id);
77
+ const path = outboxPath(home);
78
+ if (0 === remaining.length) {
79
+ writeFileSync(path, '', 'utf8');
80
+ return;
81
+ }
82
+ writeFileSync(path, `${remaining.map((entry) => JSON.stringify(entry)).join('\n')}\n`, 'utf8');
83
+ }
84
+ catch {
85
+ // A report that stays queued after a successful send is a duplicate at worst. Losing one is not.
86
+ }
87
+ }
@@ -39,8 +39,21 @@ export declare function feedbackDisabled(env?: NodeJS.ProcessEnv): boolean;
39
39
  /** What the author supplies; the environment context is detected, never asked for. */
40
40
  export type FeedbackInput = Pick<Feedback, 'source' | 'kind' | 'text' | 'trace' | 'rating' | 'need' | 'impact' | 'currentApproach' | 'model'>;
41
41
  export interface FeedbackReceipt {
42
- /** False when a kill switch is on — the report was DROPPED, and the caller is told so plainly. */
42
+ /**
43
+ * DELIVERY confirmed. Only ever true when the send was awaited and the endpoint accepted it —
44
+ * never as an optimistic stand-in for "we handed it to the emitter". That distinction is the whole
45
+ * value of this field: it was once unconditional, so a DNS miss and a 4xx both reported "filed".
46
+ *
47
+ * A backgrounded send (the agent tool) therefore returns `sent: false` with `accepted: true` —
48
+ * see below. It is not a failure, and the note says so.
49
+ */
43
50
  sent: boolean;
51
+ /**
52
+ * Validated, redacted and handed to the emitter. TRUE says the report is well-formed and on its
53
+ * way; it does NOT say it arrived. If a backgrounded send then fails, the reporter is told on its
54
+ * next tool result — see feedback-delivery.ts.
55
+ */
56
+ accepted: boolean;
44
57
  reason?: string;
45
58
  /** Which redaction rules fired. Empty when nothing needed removing. */
46
59
  redacted: string[];
@@ -56,6 +69,14 @@ export declare function submitFeedback(input: FeedbackInput, opts?: {
56
69
  cwd?: string;
57
70
  session?: SessionFacts;
58
71
  env?: NodeJS.ProcessEnv;
72
+ /**
73
+ * Return as soon as the report is validated and queued, instead of waiting out the POST.
74
+ *
75
+ * Used by the AGENT tool: ~340ms of network on a call made mid-task is the product blocking the
76
+ * user's work to talk about itself. NOT used by `reticle feedback`, where a human typed the
77
+ * command and is waiting for an answer — there, confirmed delivery is the answer.
78
+ */
79
+ background?: boolean;
59
80
  }): Promise<FeedbackReceipt>;
60
81
  /**
61
82
  * The receipt's context PLUS the fields the emitter stamps on every event regardless of kind — the
@@ -24,8 +24,11 @@
24
24
  import { FeedbackSchema, FEEDBACK_TEXT_MAX, FEEDBACK_TRACE_MAX, FEEDBACK_FIELD_MAX, TelemetryEventKind, } from '@reticlehq/core';
25
25
  import { platform } from 'node:os';
26
26
  import { getTelemetry } from './telemetry.js';
27
- import { SERVER_VERSION } from '../server-version.js';
27
+ import { noteFeedbackUndelivered } from './feedback-delivery.js';
28
+ import { isReticleSourceCheckout } from './dev-repo.js';
29
+ import { SERVER_VERSION } from '../version/server-version.js';
28
30
  import { feedbackContext } from './feedback-context.js';
31
+ import { markDelivered, outboxPath, queueFeedback } from './feedback-outbox.js';
29
32
  /** Kills the feedback channel ONLY — adoption counters keep flowing. */
30
33
  export const FEEDBACK_ENV = 'RETICLE_FEEDBACK';
31
34
  /** What replaces a redacted match. Visible on purpose: a reader must see that something was removed. */
@@ -67,7 +70,7 @@ export function redactFeedbackText(input, max) {
67
70
  removed.add(label);
68
71
  // A rule with a leading capture keeps it (the scheme, the `api_key=` key) so the reader still
69
72
  // sees WHAT was removed, not just that something was.
70
- const prefix = typeof groups[0] === 'string' ? groups[0] : '';
73
+ const prefix = 'string' === typeof groups[0] ? groups[0] : '';
71
74
  return `${prefix}${REDACTED}`;
72
75
  });
73
76
  }
@@ -76,7 +79,7 @@ export function redactFeedbackText(input, max) {
76
79
  /** True when the feedback channel is switched off on its own (the emitter's switches apply as well). */
77
80
  export function feedbackDisabled(env = process.env) {
78
81
  const value = (env[FEEDBACK_ENV] ?? '').toLowerCase();
79
- return value === '0' || value === 'false' || value === 'off' || value === 'no';
82
+ return '0' === value || 'false' === value || 'off' === value || 'no' === value;
80
83
  }
81
84
  /**
82
85
  * The fields an author writes, and therefore the fields that must be redacted. ONE list, used both to
@@ -118,7 +121,7 @@ export async function submitFeedback(input, opts = {}) {
118
121
  // addition. Deriving the set means a sixth field is covered the moment it exists.
119
122
  const redactions = new Map();
120
123
  for (const [key, value] of Object.entries(input)) {
121
- if (typeof value !== 'string' || value === '')
124
+ if (typeof value !== 'string' || '' === value)
122
125
  continue;
123
126
  if (!AUTHOR_WRITTEN_FIELDS.has(key))
124
127
  continue;
@@ -127,13 +130,25 @@ export async function submitFeedback(input, opts = {}) {
127
130
  const body = redactions.get('text') ?? { text: '', removed: [] };
128
131
  const redacted = [...new Set([...redactions.values()].flatMap((r) => r.removed))];
129
132
  if (feedbackDisabled(env)) {
130
- return { sent: false, reason: `feedback is disabled by ${FEEDBACK_ENV}`, redacted, context };
133
+ return {
134
+ sent: false,
135
+ accepted: false,
136
+ reason: `feedback is disabled by ${FEEDBACK_ENV}`,
137
+ redacted,
138
+ context,
139
+ };
131
140
  }
132
141
  const telemetry = getTelemetry();
133
142
  if (!telemetry.enabled) {
134
143
  return {
135
144
  sent: false,
136
- reason: 'telemetry is disabled on this machine, so feedback has nowhere to go',
145
+ accepted: false,
146
+ // Named precisely, because the commonest cause is not what the old wording suggested: a
147
+ // Reticle SOURCE CHECKOUT disables telemetry by cwd, and that is exactly where release runs and
148
+ // contributor sessions happen — so the reports most worth having were the ones silently lost.
149
+ reason: isReticleSourceCheckout(opts.cwd ?? process.cwd())
150
+ ? 'this is a Reticle source checkout, where telemetry is disabled by design — so feedback has nowhere to go. Open an issue at https://github.com/reticlehq/reticle/issues instead, or run from the app you are verifying.'
151
+ : `telemetry is disabled on this machine, so feedback has nowhere to go. Re-enable with \`reticle telemetry enable\`, or open an issue at https://github.com/reticlehq/reticle/issues.`,
137
152
  redacted,
138
153
  context,
139
154
  };
@@ -151,13 +166,57 @@ export async function submitFeedback(input, opts = {}) {
151
166
  if (!parsed.success) {
152
167
  return {
153
168
  sent: false,
169
+ accepted: false,
154
170
  reason: parsed.error.issues[0]?.message ?? 'invalid feedback',
155
171
  redacted,
156
172
  context,
157
173
  };
158
174
  }
159
- await telemetry.emit(TelemetryEventKind.FEEDBACK_SUBMITTED, { feedback: parsed.data });
160
- return { sent: true, redacted, context };
175
+ // BACKGROUND: return now, deliver after. The agent calls this mid-task and a ~340ms POST is the
176
+ // product blocking the user's work to talk about itself. The honesty that the awaited version was
177
+ // written for is kept elsewhere: `sent` stays false (nothing is confirmed yet), `accepted` says
178
+ // the report is well-formed and queued, and a send that then fails reaches the reporter on its
179
+ // next tool result instead of vanishing.
180
+ // Written down BEFORE the network is touched, on both paths. A failed send is then a queued report
181
+ // rather than a lost one — see feedback-outbox. Reported from the field: a 1.3s hiccup destroyed a
182
+ // root-cause analysis that took an hour of driving to produce, and it survived only because the
183
+ // reporter happened to have written the markdown by hand first.
184
+ const queued = queueFeedback(parsed.data);
185
+ if (true === opts.background) {
186
+ void telemetry
187
+ .emit(TelemetryEventKind.FEEDBACK_SUBMITTED, { feedback: parsed.data })
188
+ .then((ok) => {
189
+ if (ok)
190
+ markDelivered(queued);
191
+ else
192
+ noteFeedbackUndelivered('the telemetry endpoint did not accept it');
193
+ })
194
+ .catch((error) => {
195
+ noteFeedbackUndelivered(error instanceof Error ? error.message : String(error));
196
+ });
197
+ return { sent: false, accepted: true, redacted, context };
198
+ }
199
+ // `sent` reflects DELIVERY, not handing the payload to the emitter. It was unconditional, so a DNS
200
+ // miss or a 4xx both reported "filed" — and this is the only qualitative channel the product has,
201
+ // so a silent failure here loses the report AND tells the reporter it worked.
202
+ const delivered = await telemetry.emit(TelemetryEventKind.FEEDBACK_SUBMITTED, {
203
+ feedback: parsed.data,
204
+ });
205
+ if (delivered) {
206
+ markDelivered(queued);
207
+ return { sent: true, accepted: true, redacted, context };
208
+ }
209
+ // Undelivered, but no longer lost. `accepted` is true when it reached the outbox: the report is
210
+ // well-formed and on disk, which is a materially different situation from "it is gone".
211
+ return {
212
+ sent: false,
213
+ accepted: queued !== null,
214
+ reason: queued !== null
215
+ ? `the report could not be delivered (the network call failed or was rejected), but it is SAVED at ${outboxPath()} and nothing was lost. It will be retried; you can also open an issue at https://github.com/reticlehq/reticle/issues with the same detail.`
216
+ : 'the report could not be delivered and could not be saved locally either — it was NOT filed. Retry, or open an issue at https://github.com/reticlehq/reticle/issues with the same detail.',
217
+ redacted,
218
+ context,
219
+ };
161
220
  }
162
221
  /**
163
222
  * The receipt's context PLUS the fields the emitter stamps on every event regardless of kind — the
@@ -6,6 +6,13 @@ export interface GitFacts {
6
6
  origin?: string;
7
7
  /** Which forge hosts it, bucketed. Absent unless state is `remote`. */
8
8
  forge?: RepoForge;
9
+ /**
10
+ * The directory holding `.git` — the repo boundary. Absent only when state is `none`.
11
+ *
12
+ * Never sent: it is a local path. It exists so an UNPUSHED repo can still key its projectId to
13
+ * something stable across every directory inside it, which a raw cwd is not.
14
+ */
15
+ root?: string;
9
16
  }
10
17
  /** `git@github.com:Acme/Web.git` / `https://u:p@github.com/Acme/Web` → `github.com/acme/web`. */
11
18
  export declare const normalizeGitOrigin: (url: string) => string;
@@ -88,17 +88,17 @@ export function gitFacts(cwd, read = defaultRead, exists = defaultExists) {
88
88
  }
89
89
  catch {
90
90
  // A `.git` with no readable config is still a repo — report it as one rather than as absent.
91
- return { state: GitState.LOCAL_ONLY };
91
+ return { state: GitState.LOCAL_ONLY, root: dir };
92
92
  }
93
93
  // The url line inside the [remote "origin"] section — the first url after that header wins.
94
94
  const section = config.split(/\[remote\s+"origin"\]/)[1];
95
95
  const url = section?.match(/^\s*url\s*=\s*(.+)$/m)?.[1];
96
96
  if (url === undefined)
97
- return { state: GitState.LOCAL_ONLY };
97
+ return { state: GitState.LOCAL_ONLY, root: dir };
98
98
  const origin = normalizeGitOrigin(url);
99
- if (origin === '')
100
- return { state: GitState.LOCAL_ONLY };
101
- return { state: GitState.REMOTE, origin, forge: forgeOf(origin) };
99
+ if ('' === origin)
100
+ return { state: GitState.LOCAL_ONLY, root: dir };
101
+ return { state: GitState.REMOTE, origin, forge: forgeOf(origin), root: dir };
102
102
  }
103
103
  const parent = dirname(dir);
104
104
  if (parent === dir)
@@ -180,13 +180,14 @@ export function profileProject(cwd, now) {
180
180
  featuresUsed.push(FeatureFamily.BUG_CAPSULES);
181
181
  if (hasHistory)
182
182
  featuresUsed.push(FeatureFamily.CROSS_RUN_HISTORY);
183
- const { stack, stackMajor } = detectStack(cwd);
183
+ const { stack, stackMajor, stackSource } = detectStack(cwd);
184
184
  const ageWeeks = projectAgeWeeks(cwd, now);
185
185
  const git = gitFacts(cwd);
186
186
  return {
187
187
  git: git.state,
188
188
  ...(git.forge !== undefined ? { forge: git.forge } : {}),
189
189
  ...(stack !== undefined ? { stack } : {}),
190
+ ...(stackSource !== undefined ? { stackSource } : {}),
190
191
  ...(stackMajor !== undefined ? { stackMajor } : {}),
191
192
  size: sizeBucket(countSourceFiles(cwd)),
192
193
  monorepo: isMonorepo(cwd),
@@ -41,7 +41,15 @@ export declare class SessionMetrics {
41
41
  */
42
42
  recordBrowserLatency(ms: number): void;
43
43
  /** A call for a tool that does not exist. Non-zero means our surface is confusing the agent. */
44
- recordUnknownTool(): void;
44
+ /**
45
+ * An agent reached for a tool that does not exist — and WHAT it reached for.
46
+ *
47
+ * The name is the point. A count says the surface confused someone; the name says which
48
+ * capability they expected us to have, in their own vocabulary. Safe to keep: it is a name from
49
+ * our own namespace, never app data. Bounded and truncated, because a guess is a short
50
+ * identifier and anything longer is not one.
51
+ */
52
+ recordUnknownTool(name?: string): void;
45
53
  recordToolCall(tool: string, args?: Record<string, unknown>): void;
46
54
  /**
47
55
  * One failed tool call. Grouped by fingerprint, but stored WITH its skeleton message and the tool
@@ -62,6 +70,28 @@ export declare class SessionMetrics {
62
70
  breadcrumb: string[];
63
71
  inFlight: string | undefined;
64
72
  };
73
+ /**
74
+ * Ask the agent for a verdict, once, when it has driven the page without asking for one.
75
+ *
76
+ * Measured 2026-08-10/11: of 170 sessions that made a tool call, 140 produced no verdict — and
77
+ * **137 of those never called a verdict-producing tool even once.** They drove the app 269 times
78
+ * with `reticle_act` and never asked whether it worked. This counter already existed
79
+ * (`abandonedActions`, non-zero in 58 sessions); it was reported to US and never to the agent.
80
+ *
81
+ * Three is the threshold because the median verdict-less session made four tool calls: any higher
82
+ * and it never fires for the sessions that need it. Re-arms after a verdict, so a second
83
+ * abandoned run is caught — the loop can break more than once in a long session.
84
+ */
85
+ takeUnverifiedNudge(): string | undefined;
86
+ /**
87
+ * An app's SDK dialled this daemon.
88
+ *
89
+ * Zero of these at shutdown means the daemon ran and no app ever connected — a broken install.
90
+ * Non-zero with no tool calls is the opposite problem. See SessionSummary.appConnects.
91
+ */
92
+ recordAppConnected(): void;
93
+ /** One action driven at the page. Settled by the next verdict; see #unsettledActions. */
94
+ recordAction(): void;
65
95
  recordVerification(): void;
66
96
  /**
67
97
  * One defect found in the app under test — the outcome number, kept broken down by kind.
@@ -80,12 +110,26 @@ export declare class SessionMetrics {
80
110
  * express failure misses the only question worth asking: how often can people not get a browser?
81
111
  */
82
112
  recordConnectAttempt(kind: BrowserLaunchKind): (failure?: ConnectFailure) => void;
83
- recordClient(name: string): void;
113
+ recordClient(name: string, version?: string): void;
114
+ /**
115
+ * The agent's client went away.
116
+ *
117
+ * This is the only part of `endReason` that a query cannot derive from the counters: "the client
118
+ * detached" and "the agent stopped asking" produce identical numbers and are different findings —
119
+ * one is the task ending, the other is us losing them.
120
+ */
121
+ recordClientLeft(): void;
122
+ /** How many times in a row the current tool has been called. Read by the friction invite. */
123
+ get currentRun(): number;
124
+ /** Reticle invited the agent to send feedback. The denominator for feedback_submitted. */
125
+ recordFeedbackPrompt(): void;
126
+ /** Which tool surface was advertised to agents this session. */
127
+ recordSurface(surface: string): void;
84
128
  /**
85
129
  * Roll the counters into the event payload. `final` marks a clean shutdown; a periodic flush passes
86
130
  * false so a session killed before it can exit is not lost entirely (see the flush note in cli.ts).
87
131
  */
88
- summarize(final: boolean): SessionSummary;
132
+ summarize(final: boolean, exit?: SessionSummary['exit']): SessionSummary;
89
133
  /** True when nothing at all happened — a flush of an idle daemon is not worth an event. */
90
134
  get empty(): boolean;
91
135
  /** Zero the counters after a non-final flush so the next flush reports the NEXT window, not a total. */