@reticlehq/server 2.3.0 → 2.5.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 (424) hide show
  1. package/SKILL.md +702 -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} +1 -1
  7. package/dist/{bridge.js → bridge/bridge.js} +105 -15
  8. package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +6 -11
  9. package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +21 -9
  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-doctor.js → cli/cli-doctor.js} +9 -2
  20. package/dist/{cli-flow-commands.d.ts → cli/cli-flow-commands.d.ts} +2 -2
  21. package/dist/{cli-flow-commands.js → cli/cli-flow-commands.js} +15 -15
  22. package/dist/{cli-launch.d.ts → cli/cli-launch.d.ts} +32 -3
  23. package/dist/cli/cli-launch.js +171 -0
  24. package/dist/cli/cli-parse.d.ts +143 -0
  25. package/dist/{cli-parse.js → cli/cli-parse.js} +146 -65
  26. package/dist/{cli-port.d.ts → cli/cli-port.d.ts} +15 -0
  27. package/dist/{cli-port.js → cli/cli-port.js} +29 -8
  28. package/dist/cli/cli-update-commands.d.ts +4 -0
  29. package/dist/cli/cli-update-commands.js +93 -0
  30. package/dist/{cli-verify.js → cli/cli-verify.js} +14 -14
  31. package/dist/{cloud-cli.js → cli/cloud-cli.js} +29 -24
  32. package/dist/cli.d.ts +2 -2
  33. package/dist/cli.js +182 -93
  34. package/dist/cloud/cloud-config.d.ts +0 -1
  35. package/dist/cloud/cloud-config.js +9 -9
  36. package/dist/cloud/cloud-sync.d.ts +1 -7
  37. package/dist/cloud/cloud-sync.js +12 -12
  38. package/dist/crawl/crawl-empty.d.ts +16 -0
  39. package/dist/crawl/crawl-empty.js +24 -0
  40. package/dist/crawl/crawl-tools.js +4 -1
  41. package/dist/crawl/crawl.js +26 -7
  42. package/dist/daemon/daemon-resilience.d.ts +49 -0
  43. package/dist/daemon/daemon-resilience.js +191 -0
  44. package/dist/daemon/daemon-usefulness.d.ts +61 -0
  45. package/dist/daemon/daemon-usefulness.js +62 -0
  46. package/dist/{daemon.d.ts → daemon/daemon.d.ts} +33 -8
  47. package/dist/{daemon.js → daemon/daemon.js} +80 -48
  48. package/dist/daemon/idle-grace.d.ts +32 -0
  49. package/dist/daemon/idle-grace.js +41 -0
  50. package/dist/{idle-shutdown.d.ts → daemon/idle-shutdown.d.ts} +13 -0
  51. package/dist/{idle-shutdown.js → daemon/idle-shutdown.js} +27 -4
  52. package/dist/domain/domain-model.js +2 -2
  53. package/dist/events/body-failures.js +8 -8
  54. package/dist/events/contradictions.d.ts +1 -1
  55. package/dist/events/contradictions.js +60 -10
  56. package/dist/events/echo-mismatch.js +10 -10
  57. package/dist/events/event-filters.js +6 -6
  58. package/dist/events/json-salvage.d.ts +2 -1
  59. package/dist/events/json-salvage.js +6 -6
  60. package/dist/events/observed-in-window.d.ts +12 -0
  61. package/dist/events/observed-in-window.js +21 -0
  62. package/dist/events/predicate-eval.d.ts +43 -20
  63. package/dist/events/predicate-eval.js +159 -55
  64. package/dist/events/predicate.d.ts +2 -2
  65. package/dist/events/predicate.js +112 -35
  66. package/dist/events/reconcile.js +5 -5
  67. package/dist/events/ring-buffer.js +1 -1
  68. package/dist/events/stale-response.js +6 -6
  69. package/dist/events/testid-near-miss.d.ts +22 -0
  70. package/dist/events/testid-near-miss.js +33 -0
  71. package/dist/events/unit-mismatch.js +8 -8
  72. package/dist/flows/affected-tools.js +19 -4
  73. package/dist/flows/affected.js +1 -1
  74. package/dist/flows/annotate-target.d.ts +1 -0
  75. package/dist/flows/annotate-target.js +20 -0
  76. package/dist/flows/annotate-tools.js +25 -3
  77. package/dist/flows/annotate.js +17 -4
  78. package/dist/flows/assertion-integrity.d.ts +2 -1
  79. package/dist/flows/assertion-tiers-store.d.ts +3 -66
  80. package/dist/flows/assertion-tiers-store.js +2 -2
  81. package/dist/flows/attributed-failure.d.ts +17 -0
  82. package/dist/flows/attributed-failure.js +20 -0
  83. package/dist/flows/buddy-status.d.ts +2 -1
  84. package/dist/flows/buddy-status.js +2 -2
  85. package/dist/flows/coverage.d.ts +3 -2
  86. package/dist/flows/coverage.js +2 -2
  87. package/dist/flows/decision.d.ts +0 -6
  88. package/dist/flows/decision.js +64 -8
  89. package/dist/flows/empty-flow.d.ts +6 -0
  90. package/dist/flows/empty-flow.js +24 -0
  91. package/dist/flows/flake-store.d.ts +0 -1
  92. package/dist/flows/flake-store.js +1 -1
  93. package/dist/flows/flake.d.ts +2 -1
  94. package/dist/flows/flake.js +2 -2
  95. package/dist/flows/flow-classify.js +12 -0
  96. package/dist/flows/flow-replay-run.js +4 -4
  97. package/dist/flows/flow-replay.d.ts +5 -7
  98. package/dist/flows/flow-replay.js +114 -47
  99. package/dist/flows/flow-report.js +1 -1
  100. package/dist/flows/flow-scope.d.ts +1 -6
  101. package/dist/flows/flow-step-runners.d.ts +26 -6
  102. package/dist/flows/flow-step-runners.js +157 -6
  103. package/dist/flows/flow-success.js +20 -15
  104. package/dist/flows/flow-tools.js +67 -13
  105. package/dist/flows/flows.js +15 -11
  106. package/dist/flows/gate.d.ts +4 -3
  107. package/dist/flows/gate.js +1 -1
  108. package/dist/flows/heal-run.d.ts +0 -8
  109. package/dist/flows/heal-run.js +5 -5
  110. package/dist/flows/heal.js +3 -3
  111. package/dist/flows/predicate-to-expect.d.ts +38 -0
  112. package/dist/flows/predicate-to-expect.js +95 -0
  113. package/dist/flows/replay.d.ts +16 -1
  114. package/dist/flows/replay.js +58 -22
  115. package/dist/flows/role-anchor-nearest.d.ts +6 -0
  116. package/dist/flows/role-anchor-nearest.js +45 -0
  117. package/dist/flows/server-verify.d.ts +1 -1
  118. package/dist/flows/server-verify.js +5 -5
  119. package/dist/flows/verify-change-tools.js +32 -10
  120. package/dist/flows/verify-change-verdict.d.ts +15 -0
  121. package/dist/flows/verify-change-verdict.js +25 -0
  122. package/dist/flows/watch-batcher.d.ts +3 -2
  123. package/dist/honesty/already-true.d.ts +2 -0
  124. package/dist/honesty/already-true.js +31 -0
  125. package/dist/honesty/blind-spots.d.ts +16 -3
  126. package/dist/honesty/blind-spots.js +24 -12
  127. package/dist/honesty/honesty.d.ts +3 -2
  128. package/dist/honesty/honesty.js +4 -4
  129. package/dist/honesty/uncaptured-bodies.js +1 -1
  130. package/dist/honesty/unread-outcome.js +2 -2
  131. package/dist/honesty/verified.d.ts +22 -3
  132. package/dist/honesty/verified.js +48 -8
  133. package/dist/http-server.js +14 -7
  134. package/dist/hunt/hunt-report.d.ts +2 -1
  135. package/dist/hunt/hunt-report.js +2 -2
  136. package/dist/index.d.ts +8 -6
  137. package/dist/index.js +32 -25
  138. package/dist/init/agent-rules.d.ts +2 -1
  139. package/dist/init/agent-rules.js +45 -7
  140. package/dist/init/app-choice.d.ts +20 -0
  141. package/dist/init/app-choice.js +25 -0
  142. package/dist/init/astro-host.d.ts +21 -0
  143. package/dist/init/astro-host.js +27 -0
  144. package/dist/init/astro-patch.d.ts +16 -0
  145. package/dist/init/astro-patch.js +240 -0
  146. package/dist/init/capabilities.d.ts +17 -0
  147. package/dist/init/capabilities.js +54 -0
  148. package/dist/init/cra.d.ts +37 -0
  149. package/dist/init/cra.js +90 -0
  150. package/dist/init/cursor.js +39 -4
  151. package/dist/init/desktop-doctor.d.ts +3 -2
  152. package/dist/init/desktop-doctor.js +4 -4
  153. package/dist/init/detect.d.ts +26 -0
  154. package/dist/init/detect.js +78 -5
  155. package/dist/init/mcp-registered.d.ts +12 -0
  156. package/dist/init/mcp-registered.js +14 -0
  157. package/dist/init/mcp.d.ts +27 -2
  158. package/dist/init/mcp.js +40 -4
  159. package/dist/init/next-patch.d.ts +39 -0
  160. package/dist/init/next-patch.js +196 -0
  161. package/dist/init/node-io.js +48 -6
  162. package/dist/init/patch-kind.d.ts +20 -0
  163. package/dist/init/patch-kind.js +10 -0
  164. package/dist/init/plan-framework.d.ts +50 -0
  165. package/dist/init/plan-framework.js +341 -0
  166. package/dist/init/plan.d.ts +119 -6
  167. package/dist/init/plan.js +230 -131
  168. package/dist/init/project-id.d.ts +5 -9
  169. package/dist/init/project-id.js +10 -22
  170. package/dist/init/run.d.ts +22 -0
  171. package/dist/init/run.js +393 -28
  172. package/dist/init/slash-command.d.ts +27 -0
  173. package/dist/init/slash-command.js +61 -0
  174. package/dist/init/snippets.d.ts +41 -3
  175. package/dist/init/snippets.js +185 -10
  176. package/dist/init/vite-config.d.ts +4 -10
  177. package/dist/init/vite-config.js +15 -7
  178. package/dist/init/workspace-apps.d.ts +23 -0
  179. package/dist/init/workspace-apps.js +68 -0
  180. package/dist/input/driven-redaction.d.ts +15 -0
  181. package/dist/input/driven-redaction.js +76 -0
  182. package/dist/input/network-detail.d.ts +9 -3
  183. package/dist/input/network-detail.js +43 -23
  184. package/dist/input/network-mock-tools.js +2 -2
  185. package/dist/input/network-mock.js +4 -4
  186. package/dist/input/real-input.js +8 -3
  187. package/dist/input/scroll-find.js +2 -2
  188. package/dist/input/scroll-tools.d.ts +0 -5
  189. package/dist/input/scroll-tools.js +11 -4
  190. package/dist/input/viewport-tools.js +2 -2
  191. package/dist/journal/ambient-store.d.ts +0 -2
  192. package/dist/journal/ambient-store.js +1 -1
  193. package/dist/journal/ambient.js +1 -1
  194. package/dist/journal/attach-journal.d.ts +2 -1
  195. package/dist/journal/deviation-report.js +6 -6
  196. package/dist/journal/deviation-service.js +1 -1
  197. package/dist/journal/envelope-store.d.ts +0 -2
  198. package/dist/journal/envelope-store.js +1 -1
  199. package/dist/journal/envelope.d.ts +2 -3
  200. package/dist/journal/envelope.js +3 -3
  201. package/dist/journal/journal-query.js +1 -1
  202. package/dist/journal/journal-recorder.d.ts +2 -1
  203. package/dist/journal/journal-recorder.js +2 -2
  204. package/dist/journal/rollups.js +9 -9
  205. package/dist/journal/session-end.d.ts +2 -1
  206. package/dist/journal/session-journal.js +4 -4
  207. package/dist/license/license.d.ts +0 -14
  208. package/dist/license/license.js +8 -8
  209. package/dist/log.d.ts +10 -0
  210. package/dist/log.js +11 -1
  211. package/dist/mcp/mcp-is-error.d.ts +13 -0
  212. package/dist/mcp/mcp-is-error.js +18 -0
  213. package/dist/mcp/mcp-outage.d.ts +31 -0
  214. package/dist/mcp/mcp-outage.js +48 -0
  215. package/dist/mcp/mcp-proxy.d.ts +124 -0
  216. package/dist/mcp/mcp-proxy.js +697 -0
  217. package/dist/{mcp.d.ts → mcp/mcp.d.ts} +13 -10
  218. package/dist/{mcp.js → mcp/mcp.js} +110 -35
  219. package/dist/mcp/proxy-handshake.d.ts +3 -0
  220. package/dist/mcp/proxy-handshake.js +54 -0
  221. package/dist/mcp/proxy-lifecycle.d.ts +64 -0
  222. package/dist/mcp/proxy-lifecycle.js +70 -0
  223. package/dist/mcp/tool-catalog-cache.d.ts +29 -0
  224. package/dist/mcp/tool-catalog-cache.js +55 -0
  225. package/dist/oracles/propose-consequences.d.ts +2 -1
  226. package/dist/oracles/propose-consequences.js +16 -11
  227. package/dist/oracles/self-instrument.d.ts +2 -1
  228. package/dist/oracles/self-instrument.js +1 -1
  229. package/dist/phenomena/phenomena.js +3 -3
  230. package/dist/platform.d.ts +16 -0
  231. package/dist/platform.js +16 -0
  232. package/dist/pool/playwright-launcher.d.ts +15 -2
  233. package/dist/pool/playwright-launcher.js +21 -2
  234. package/dist/project/fs-port.js +1 -1
  235. package/dist/project/project-tools.js +2 -2
  236. package/dist/runs/build-verification-run.js +1 -1
  237. package/dist/runs/repair-prompt.d.ts +0 -6
  238. package/dist/runs/repair-prompt.js +3 -3
  239. package/dist/runs/risk-classify.js +1 -1
  240. package/dist/runs/run-diff.d.ts +0 -2
  241. package/dist/runs/run-diff.js +3 -3
  242. package/dist/runs/run-tools.js +2 -2
  243. package/dist/runs/verification-sync.js +2 -2
  244. package/dist/runs/verify-http.js +1 -1
  245. package/dist/runs/verify-server.js +1 -1
  246. package/dist/session/command-timeout.d.ts +11 -3
  247. package/dist/session/command-timeout.js +2 -2
  248. package/dist/session/control-envelope.d.ts +6 -0
  249. package/dist/session/control-envelope.js +7 -1
  250. package/dist/session/dev-server-probe.d.ts +13 -0
  251. package/dist/session/dev-server-probe.js +52 -0
  252. package/dist/session/last-act.d.ts +17 -3
  253. package/dist/session/last-act.js +16 -4
  254. package/dist/session/live-control-tools.js +7 -2
  255. package/dist/session/live-control.js +1 -1
  256. package/dist/session/looks-like-dev-server.d.ts +14 -0
  257. package/dist/session/looks-like-dev-server.js +25 -0
  258. package/dist/session/no-session-diagnosis.d.ts +38 -0
  259. package/dist/session/no-session-diagnosis.js +70 -0
  260. package/dist/session/no-session-watch.d.ts +19 -0
  261. package/dist/session/no-session-watch.js +61 -0
  262. package/dist/session/observed-state.d.ts +4 -0
  263. package/dist/session/observed-state.js +19 -1
  264. package/dist/session/output-budget.js +1 -1
  265. package/dist/session/session-health.d.ts +8 -1
  266. package/dist/session/session-health.js +8 -7
  267. package/dist/session/session-info.d.ts +26 -7
  268. package/dist/session/session-info.js +42 -1
  269. package/dist/session/session-lease.d.ts +0 -11
  270. package/dist/session/session-lease.js +1 -1
  271. package/dist/session/session-manager.d.ts +3 -0
  272. package/dist/session/session-manager.js +58 -9
  273. package/dist/session/session-readiness.d.ts +1 -1
  274. package/dist/session/session-readiness.js +6 -1
  275. package/dist/session/session-reaper.js +1 -1
  276. package/dist/session/session-reconnect.d.ts +32 -0
  277. package/dist/session/session-reconnect.js +42 -0
  278. package/dist/session/session.d.ts +20 -2
  279. package/dist/session/session.js +76 -26
  280. package/dist/session/state-select.d.ts +1 -1
  281. package/dist/session/state-select.js +1 -1
  282. package/dist/status-payload.d.ts +19 -0
  283. package/dist/status-payload.js +11 -0
  284. package/dist/telemetry/argument-shape.js +10 -1
  285. package/dist/telemetry/browser-mode.d.ts +27 -0
  286. package/dist/telemetry/browser-mode.js +33 -0
  287. package/dist/telemetry/bug-found.d.ts +17 -21
  288. package/dist/telemetry/bug-found.js +120 -8
  289. package/dist/telemetry/cli-telemetry.d.ts +2 -12
  290. package/dist/telemetry/cli-telemetry.js +27 -4
  291. package/dist/telemetry/daemon-telemetry.d.ts +19 -0
  292. package/dist/telemetry/daemon-telemetry.js +12 -2
  293. package/dist/telemetry/dev-repo.js +4 -4
  294. package/dist/telemetry/error-fingerprint.js +21 -2
  295. package/dist/telemetry/feedback-cli.d.ts +1 -1
  296. package/dist/telemetry/feedback-cli.js +4 -4
  297. package/dist/telemetry/feedback-context.d.ts +22 -0
  298. package/dist/telemetry/feedback-context.js +41 -1
  299. package/dist/telemetry/feedback-delivery.d.ts +20 -0
  300. package/dist/telemetry/feedback-delivery.js +30 -0
  301. package/dist/telemetry/feedback-outbox.d.ts +22 -0
  302. package/dist/telemetry/feedback-outbox.js +87 -0
  303. package/dist/telemetry/feedback.d.ts +22 -1
  304. package/dist/telemetry/feedback.js +67 -8
  305. package/dist/telemetry/git-facts.js +1 -1
  306. package/dist/telemetry/session-metrics.d.ts +2 -0
  307. package/dist/telemetry/session-metrics.js +68 -6
  308. package/dist/telemetry/telemetry.d.ts +27 -2
  309. package/dist/telemetry/telemetry.js +119 -29
  310. package/dist/telemetry/verification-of.d.ts +22 -0
  311. package/dist/telemetry/verification-of.js +84 -0
  312. package/dist/tools/act-capsule.js +2 -2
  313. package/dist/tools/act-danger.d.ts +12 -0
  314. package/dist/tools/act-danger.js +20 -3
  315. package/dist/tools/act-helpers.js +4 -4
  316. package/dist/tools/act-tools.d.ts +0 -8
  317. package/dist/tools/act-tools.js +84 -115
  318. package/dist/tools/act-view.js +7 -7
  319. package/dist/tools/alias-args.d.ts +17 -0
  320. package/dist/tools/alias-args.js +25 -0
  321. package/dist/tools/assert-grade.js +9 -9
  322. package/dist/tools/assert-verdict.d.ts +3 -1
  323. package/dist/tools/assert-verdict.js +8 -5
  324. package/dist/tools/browser-tools.js +30 -11
  325. package/dist/tools/contract-tools.js +1 -1
  326. package/dist/tools/coverage-identity.d.ts +34 -0
  327. package/dist/tools/coverage-identity.js +46 -0
  328. package/dist/tools/coverage-tools.js +10 -14
  329. package/dist/tools/dynamic-tools.d.ts +2 -1
  330. package/dist/tools/dynamic-tools.js +90 -18
  331. package/dist/tools/error-recovery.d.ts +20 -2
  332. package/dist/tools/error-recovery.js +179 -4
  333. package/dist/tools/feedback-tools.d.ts +12 -1
  334. package/dist/tools/feedback-tools.js +28 -5
  335. package/dist/tools/invoke-tool.d.ts +0 -6
  336. package/dist/tools/invoke-tool.js +57 -32
  337. package/dist/tools/lease-hint.d.ts +17 -0
  338. package/dist/tools/lease-hint.js +24 -0
  339. package/dist/tools/lease-tools.js +6 -5
  340. package/dist/tools/merge-tools.d.ts +10 -1
  341. package/dist/tools/merge-tools.js +9 -3
  342. package/dist/tools/merged-name-redirect.d.ts +13 -0
  343. package/dist/tools/merged-name-redirect.js +55 -0
  344. package/dist/tools/navigate-result.d.ts +17 -0
  345. package/dist/tools/navigate-result.js +31 -0
  346. package/dist/tools/observe-tools.js +54 -20
  347. package/dist/tools/observed-nothing.d.ts +25 -0
  348. package/dist/tools/observed-nothing.js +33 -0
  349. package/dist/tools/query-paginate.js +2 -2
  350. package/dist/tools/query-shape.d.ts +18 -0
  351. package/dist/tools/query-shape.js +39 -0
  352. package/dist/tools/read-tools.js +85 -20
  353. package/dist/tools/real-input-attempt.d.ts +28 -0
  354. package/dist/tools/real-input-attempt.js +88 -0
  355. package/dist/tools/reconcile-tools.js +7 -7
  356. package/dist/tools/reload-result.d.ts +14 -0
  357. package/dist/tools/reload-result.js +29 -0
  358. package/dist/tools/snapshot-delta.d.ts +4 -5
  359. package/dist/tools/snapshot-delta.js +0 -0
  360. package/dist/tools/tool-kit.d.ts +8 -0
  361. package/dist/tools/tool-surface.d.ts +83 -0
  362. package/dist/tools/tool-surface.js +204 -0
  363. package/dist/tools/tools-helpers.js +6 -6
  364. package/dist/tools/tools.d.ts +15 -0
  365. package/dist/tools/tools.js +53 -17
  366. package/dist/tools/unadvertised-help.d.ts +5 -0
  367. package/dist/tools/unadvertised-help.js +39 -0
  368. package/dist/trace.d.ts +69 -0
  369. package/dist/trace.js +150 -0
  370. package/dist/update/nudge-credit.d.ts +4 -0
  371. package/dist/update/nudge-credit.js +53 -0
  372. package/dist/update/sdk-sync.d.ts +21 -0
  373. package/dist/update/sdk-sync.js +42 -0
  374. package/dist/update/update-checker.js +3 -3
  375. package/dist/update/update-nudge.d.ts +63 -6
  376. package/dist/update/update-nudge.js +128 -15
  377. package/dist/update/updater.d.ts +1 -9
  378. package/dist/update/updater.js +13 -8
  379. package/dist/version/peer-announce.d.ts +4 -0
  380. package/dist/version/peer-announce.js +30 -0
  381. package/dist/{server-version.js → version/server-version.js} +5 -1
  382. package/dist/version/version-nudge.d.ts +28 -0
  383. package/dist/version/version-nudge.js +40 -0
  384. package/dist/version/version-skew.d.ts +66 -0
  385. package/dist/version/version-skew.js +71 -0
  386. package/dist/visual/visual-diff.js +2 -2
  387. package/dist/visual/visual-tools.js +4 -4
  388. package/docs/agent-cheatsheet.md +85 -0
  389. package/docs/architecture.md +113 -0
  390. package/docs/benchmarks.md +255 -0
  391. package/docs/debugging.md +102 -0
  392. package/docs/deploy-checks.md +69 -0
  393. package/docs/desktop-apps.md +256 -0
  394. package/docs/enterprise.md +62 -0
  395. package/docs/fixtures.md +28 -0
  396. package/docs/flows.md +166 -0
  397. package/docs/getting-started.md +469 -0
  398. package/docs/human-control.md +63 -0
  399. package/docs/integration-patterns.md +171 -0
  400. package/docs/local-registry.md +126 -0
  401. package/docs/mint.json +61 -0
  402. package/docs/multi-agent-testing.md +55 -0
  403. package/docs/platform-integration.md +122 -0
  404. package/docs/telemetry-contract.md +214 -0
  405. package/docs/telemetry.md +184 -0
  406. package/docs/testing.md +82 -0
  407. package/docs/token-efficiency.md +80 -0
  408. package/docs/usage.md +1046 -0
  409. package/package.json +8 -5
  410. package/dist/cli-launch.js +0 -112
  411. package/dist/cli-parse.d.ts +0 -140
  412. package/dist/daemon-resilience.d.ts +0 -22
  413. package/dist/daemon-resilience.js +0 -85
  414. package/dist/mcp-proxy.d.ts +0 -35
  415. package/dist/mcp-proxy.js +0 -190
  416. package/dist/tools/profiles.d.ts +0 -37
  417. package/dist/tools/profiles.js +0 -139
  418. /package/dist/{pairing-token.d.ts → bridge/pairing-token.d.ts} +0 -0
  419. /package/dist/{token-auth.d.ts → bridge/token-auth.d.ts} +0 -0
  420. /package/dist/{token-auth.js → bridge/token-auth.js} +0 -0
  421. /package/dist/{cli-doctor.d.ts → cli/cli-doctor.d.ts} +0 -0
  422. /package/dist/{cli-verify.d.ts → cli/cli-verify.d.ts} +0 -0
  423. /package/dist/{cloud-cli.d.ts → cli/cloud-cli.d.ts} +0 -0
  424. /package/dist/{server-version.d.ts → version/server-version.d.ts} +0 -0
package/dist/index.js CHANGED
@@ -2,10 +2,11 @@ import { join } from 'node:path';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { AGENT_STOPPED_NOTICE, RETICLE_DEFAULT_PORT, ReticleCommand, ReticleDir, ReticleEnv, LOOPBACK_HOST, ReplayStatus, EventType, } from '@reticlehq/core';
4
4
  import { originOf } from './session/session-manager.js';
5
+ import { setBrowserMode, BrowserMode } from './telemetry/browser-mode.js';
5
6
  import { replayNamedFlow } from './flows/flow-tools.js';
6
7
  import { createSharedServer } from './http-server.js';
7
- import { resolveBridgeSecurityWithAutoToken } from './bridge-security.js';
8
- import { Bridge } from './bridge.js';
8
+ import { resolveBridgeSecurityWithAutoToken } from './bridge/bridge-security.js';
9
+ import { Bridge } from './bridge/bridge.js';
9
10
  import { BaselineStore } from './project/baselines.js';
10
11
  import { RecordingStore } from './flows/recordings.js';
11
12
  import { FlowStore } from './flows/flows.js';
@@ -17,15 +18,18 @@ import { ReticleRunner } from './runs/reticle-runner.js';
17
18
  import { createRunnerPort } from './runs/runner-port.js';
18
19
  import { RunStore } from './runs/run-store.js';
19
20
  import { startVerifyServer } from './runs/verify-server.js';
20
- import { createMcpServer } from './mcp.js';
21
+ import { createMcpServer } from './mcp/mcp.js';
21
22
  import { SessionReaper, endAllSessions, MCP_DISCONNECT_SUMMARY } from './session/session-reaper.js';
22
- import { resolveToolProfile } from './tools/profiles.js';
23
+ import { wireSessionScope } from './session/no-session-watch.js';
24
+ import { buildIdlePredicate } from './daemon/daemon-usefulness.js';
25
+ import { resolveToolSurface } from './tools/tool-surface.js';
26
+ import { statusPayload } from './status-payload.js';
23
27
  import { CdpRealInputProvider, LaunchedRealInputProvider } from './input/real-input.js';
24
28
  import { cpus } from 'node:os';
25
29
  import { BrowserPool } from './pool/browser-pool.js';
26
30
  import { playwrightLauncher, resolveMaxContexts } from './pool/playwright-launcher.js';
27
31
  import { LeaseReaper } from './pool/lease-reaper.js';
28
- import { readJournalEnabled, readProjectId } from './cli-port.js';
32
+ import { readJournalEnabled, readProjectId } from './cli/cli-port.js';
29
33
  import { makeJournalAttach } from './journal/attach-journal.js';
30
34
  import { makeSessionEnd } from './journal/session-end.js';
31
35
  import { AmbientStore } from './journal/ambient-store.js';
@@ -41,7 +45,7 @@ function replayVerdictLine(result) {
41
45
  }
42
46
  export { ReticleTool } from './tools/tool-names.js';
43
47
  export { RingBuffer } from './events/ring-buffer.js';
44
- export { Bridge } from './bridge.js';
48
+ export { Bridge } from './bridge/bridge.js';
45
49
  export { Session, SessionManager } from './session/session.js';
46
50
  export { buildSessionRecommendation } from './session/session-recommendation.js';
47
51
  export { TOOLS } from './tools/tools.js';
@@ -61,9 +65,9 @@ export { MCP_SSE_PATH, MCP_MESSAGE_PATH } from '@reticlehq/core';
61
65
  export { BrowserPool, DEFAULT_LEASE_TTL_MS } from './pool/browser-pool.js';
62
66
  export { playwrightLauncher, resolveMaxContexts } from './pool/playwright-launcher.js';
63
67
  export { appendReticleParams } from './tools/lease-tools.js';
64
- export { writePid, removePid, isRunning, logPath, readPid, isAlive } from './daemon.js';
68
+ export { writePid, removePid, isRunning, logPath, readPid, isAlive } from './daemon/daemon.js';
65
69
  export { scrollToFind } from './input/scroll-find.js';
66
- export { CORE_TOOL_NAMES, TOOL_PROFILE, TOOL_PROFILE_ENV, filterTools, resolveToolProfile, } from './tools/profiles.js';
70
+ export { CORE_TOOL_NAMES, TOOL_SURFACE, TOOL_PROFILE_ENV, filterTools, resolveToolSurface, } from './tools/tool-surface.js';
67
71
  export { AnnotationStore } from './flows/annotation-store.js';
68
72
  export { replayFlow, nearestTestid } from './flows/flow-replay.js';
69
73
  export { ensureReticleDir, writeContract, readContract, reticleDirPaths, flowPath, baselinePath, } from './project/reticle-dir.js';
@@ -84,14 +88,14 @@ export { buildReactionReport } from './events/reaction.js';
84
88
  export { CdpRealInputProvider, LaunchedRealInputProvider, DriveError, performGesture, boxCenter, isPointerAction, } from './input/real-input.js';
85
89
  /** Default localhost port for the verify HTTP endpoint (see docs/platform-integration.md). */
86
90
  export const RETICLE_VERIFY_DEFAULT_PORT = 7331;
87
- export { resolveBridgeSecurity } from './bridge-security.js';
91
+ export { resolveBridgeSecurity } from './bridge/bridge-security.js';
88
92
  /**
89
93
  * Build the shared browser pool (one headless Chromium, N capped isolated leased contexts). Lazy —
90
94
  * no Chromium launches until the first lease — so creating it is free even when never used.
91
95
  */
92
96
  function createBrowserPool(headless) {
93
97
  const maxContexts = resolveMaxContexts(process.env[ReticleEnv.MAX_CONTEXTS], cpus().length);
94
- const genSessionId = () => `lease-${typeof globalThis.crypto?.randomUUID === 'function'
98
+ const genSessionId = () => `lease-${'function' === typeof globalThis.crypto?.randomUUID
95
99
  ? globalThis.crypto.randomUUID()
96
100
  : String(Date.now())}`;
97
101
  return new BrowserPool(playwrightLauncher({ headless }), { maxContexts, genSessionId });
@@ -174,6 +178,9 @@ async function resolveRealInput(options, onNavigateError, onNetworkDetail) {
174
178
  const driveUrl = options.driveUrl;
175
179
  if (driveUrl !== undefined && driveUrl.length > 0) {
176
180
  const headless = options.headless ?? true;
181
+ // The one place that knows. Everything downstream reads it rather than re-deriving it, and a
182
+ // daemon that launches nothing keeps the default (ATTACHED) rather than guessing.
183
+ setBrowserMode(headless ? BrowserMode.HEADLESS : BrowserMode.HEADED);
177
184
  const injectConnect = options.injectConnect;
178
185
  const storageState = options.storageState;
179
186
  const factory = options.realInputFactory ??
@@ -254,8 +261,9 @@ export async function start(options = {}) {
254
261
  fs,
255
262
  reticleRoot,
256
263
  now,
264
+ bridgePort: port,
257
265
  };
258
- const profile = resolveToolProfile(options.toolProfile);
266
+ const profile = resolveToolSurface(options.toolProfile);
259
267
  const server = createMcpServer(realInput !== undefined ? { ...deps, realInput } : deps, profile);
260
268
  // When the agent (the MCP client) disconnects cleanly, end every active session at once so the
261
269
  // HUD doesn't linger. (If the agent instead KILLS this process, the WS dies and the browser
@@ -294,11 +302,7 @@ export async function startDaemon(options = {}) {
294
302
  // mirror rejection so a port collision can't surface as an unhandled promise rejection.
295
303
  void bridge.ready.catch(() => undefined);
296
304
  // `reticle status` GETs this for a live, at-a-glance view of connected tabs + their health.
297
- shared.attachStatus(() => ({
298
- running: true,
299
- sessionCount: bridge.sessions.count(),
300
- sessions: bridge.sessions.list(),
301
- }));
305
+ shared.attachStatus(() => statusPayload(bridge.sessions.count(), bridge.sessions.list()));
302
306
  // Agent-independent presence: the daemon outlives any single agent, so when the LAST agent's MCP
303
307
  // connection drops (it stopped, or is waiting on the human), end every session and push a clear
304
308
  // "go to your terminal" notice to the panel — the human is on the browser and must not lose a typed
@@ -315,10 +319,9 @@ export async function startDaemon(options = {}) {
315
319
  reaper.start();
316
320
  // Scope auto-selection to the active project (from .reticle.json) so a stray tab from another app is
317
321
  // never picked when the agent omits a sessionId. Explicit per-call scope/sessionId still overrides.
318
- const activeProjectId = readProjectId(process.cwd());
319
- if (activeProjectId !== undefined) {
320
- bridge.sessions.setDefaultScope({ projectId: activeProjectId });
321
- }
322
+ // Scope + the no-session diagnosis: "no browser session connected" is the error that ends most
323
+ // sessions, and the agent is told to check two things it cannot see. See no-session-diagnosis.ts.
324
+ wireSessionScope(bridge.sessions, readProjectId(process.cwd()), port);
322
325
  const { realInput, owned } = await resolveRealInput(options, () => shared.close(), makeNetworkDetailRouter(bridge, options.driveUrl));
323
326
  const fs = createNodeFileSystem();
324
327
  const reticleRoot = options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT);
@@ -342,15 +345,16 @@ export async function startDaemon(options = {}) {
342
345
  fs,
343
346
  reticleRoot,
344
347
  now,
348
+ bridgePort: port,
345
349
  };
346
- const profile = resolveToolProfile(options.toolProfile);
350
+ const profile = resolveToolSurface(options.toolProfile);
347
351
  const effectiveDeps = realInput !== undefined ? { ...deps, realInput } : deps;
348
352
  shared.attachMcp(() => createMcpServer(effectiveDeps, profile));
349
353
  // Optional OEM/CI verify endpoint: a host platform POSTs to /verify and gets an ReticleVerificationRun,
350
354
  // driving the same flow-replay machinery the agent uses — no MCP stdio, no human. Each verdict is
351
355
  // persisted via RunStore. Localhost-bound + token-guarded. Off unless `reticle serve --http`.
352
356
  let verifyHttp;
353
- if (options.httpVerify === true) {
357
+ if (true === options.httpVerify) {
354
358
  const runStore = new RunStore(fs, reticleRoot);
355
359
  const runner = new ReticleRunner(createRunnerPort(effectiveDeps));
356
360
  const token = options.httpVerifyToken ?? process.env[ReticleEnv.VERIFY_TOKEN] ?? '';
@@ -410,9 +414,12 @@ export async function startDaemon(options = {}) {
410
414
  ...(realInput !== undefined ? { realInput } : {}),
411
415
  ...(verifyHttp !== undefined ? { verifyPort: verifyHttp.port } : {}),
412
416
  ...(security.token !== undefined ? { token: security.token } : {}),
413
- // Idle = nobody is using the daemon: no agent attached, no browser tab connected, no pool lease.
414
- // The daemon entry self-shuts-down after this holds for the grace window (see cli.ts / IdleShutdown).
415
- isIdle: () => !agentConnected && bridge.sessions.count() === 0 && pool.activeCount() === 0,
417
+ // Nobody is using this daemon — self-shuts-down once it holds for the grace window. See
418
+ // daemon-usefulness.ts for the two ways that can be true.
419
+ isIdle: buildIdlePredicate(() => agentConnected, bridge.sessions, pool),
420
+ // Exposed so the shutdown watcher can give an ATTACHED daemon a longer grace. The predicate above
421
+ // says WHETHER it is idle; this says how long that quiet should be tolerated — see idle-grace.
422
+ agentAttached: () => agentConnected,
416
423
  close: async () => {
417
424
  reaper.stop();
418
425
  const vh = verifyHttp;
@@ -20,7 +20,7 @@ export declare const AgentRuleStatus: {
20
20
  readonly ALREADY: "already";
21
21
  };
22
22
  export type AgentRuleStatus = (typeof AgentRuleStatus)[keyof typeof AgentRuleStatus];
23
- export interface AgentRuleResult {
23
+ interface AgentRuleResult {
24
24
  status: AgentRuleStatus;
25
25
  /** Full file content to write when status is APPLY; the unchanged input otherwise. */
26
26
  content: string;
@@ -35,3 +35,4 @@ export declare function markedBlock(): string;
35
35
  export declare function mergeMarkedInstruction(existing: string | null | undefined): AgentRuleResult;
36
36
  /** The Cursor rule file (.mdc): `alwaysApply` keeps the rule in every turn's context. */
37
37
  export declare function cursorRuleFile(): string;
38
+ export {};
@@ -10,6 +10,8 @@
10
10
  * cross-agent fallback. Idempotent via a marker block — safe to append to an existing file and a
11
11
  * re-run is a no-op. Mirrors the apply/already result shape used across init (see cursor.ts).
12
12
  */
13
+ import { NPX } from './mcp.js';
14
+ import { RETICLE_NPM_PACKAGE } from '../version/server-version.js';
13
15
  /** Project-root instruction files the agent re-reads every session. */
14
16
  export const CLAUDE_MD_PATH = 'CLAUDE.md';
15
17
  export const AGENTS_MD_PATH = 'AGENTS.md';
@@ -18,19 +20,31 @@ export const CURSOR_RULE_PATH = '.cursor/rules/reticle.mdc';
18
20
  /** Markers delimiting the managed block so a re-run is idempotent and the block stays updatable. */
19
21
  const RULE_BEGIN = '<!-- reticle:begin (managed by `reticle init` — edit outside these markers) -->';
20
22
  const RULE_END = '<!-- reticle:end -->';
23
+ /**
24
+ * How the agent must INVOKE the CLI.
25
+ *
26
+ * `reticle init` installs the SDK packages, never the server — so there is no `reticle` on the
27
+ * agent's PATH, and every bare `reticle <cmd>` this rule named was an instruction that could not be
28
+ * followed. Reported from a field sweep: three of them, in the file the agent re-reads every session.
29
+ *
30
+ * `npx @reticlehq/server` is the same invocation the MCP registration uses, and it is derived from
31
+ * the package's own name so the two can never drift apart.
32
+ */
33
+ const CLI = `${NPX} ${RETICLE_NPM_PACKAGE}`;
21
34
  /** The rule body (markdown). States WHEN to verify, HOW, and never-weaken-a-check. */
22
35
  const RULE_BODY = `## Verifying with Reticle
23
36
 
24
- This app is instrumented by **Reticle** — an in-app verification layer exposed as \`reticle_*\` MCP tools and the \`reticle\` CLI. Verifying is part of "done", not an optional extra.
37
+ This app is instrumented by **Reticle** — an in-app verification layer exposed as \`reticle_*\` MCP tools and the \`${CLI}\` CLI (run it through npx — Reticle's server is not installed into this project). Verifying is part of "done", not an optional extra.
25
38
 
26
39
  - **When:** after you build or change any user-facing feature, flow, or UI, verify it with Reticle **before** telling the user it is complete. Reading the code or passing unit tests is not enough — drive the real running app.
27
40
  - **How:** drive the actual flow (\`reticle_act\` / \`reticle_act_and_wait\`), then read the observable effect (\`reticle_assert\`, \`reticle_snapshot\`, \`reticle_state\`, \`reticle_network\`). Confirm the change produced the effect it was meant to, with no new console errors or failed requests.
28
- - **Covered flows:** run \`reticle gate\` — it reports which recorded flows the changed files affect and whether they still pass.
41
+ - **Covered flows:** run \`${CLI} gate\` — it reports which recorded flows the changed files affect and whether they still pass.
29
42
  - **Never weaken a check to make it green.** Downgrading, skipping, or deleting an assertion is a finding, not a fix.
30
43
  - **If Reticle can't run** (no daemon, or this is not a running web app), say so — do not skip verification silently.
31
- - **If a tool result carries \`update_available\`, say so.** Reticle checked npm and a newer version exists. Tell the human the version numbers and offer to run \`reticle update\` — between tasks, never mid-verification, because it restarts the daemon and drops the session. Finish what you are doing first.
32
- - **Wish Reticle could do something? Ask for it with \`reticle_feedback\`.** Reticle is built for agents, so what you find awkward or missing is what decides the roadmap — and you are the only one who can report it, because that thought disappears when your context does. File \`feature_request\` for something that does not exist, \`improvement\` for something that works but costs too many calls. Say what you are actually trying to achieve (\`need\`) and how you work around it today (\`currentApproach\`), and set \`model\` to the model you are running. File it the moment you notice, then carry on — nothing is too minor.
33
- - **If Reticle itself fails you, tell its maintainers with \`reticle_feedback\`.** A tool that returned something wrong, a thing Reticle could not observe at all, or a verdict you could not act on — file it with a root-cause analysis and the call trace, then carry on with the task. Working around a broken tool in silence is how the same defect survives. This is about defects in *Reticle*; a bug you find in *this app* is Reticle working, and belongs in your report to the user instead.`;
44
+ - **If a tool result carries \`version_skew\`, act on it before trusting anything else.** Two pieces of Reticle are on different wire contracts, so tools can misbehave in ways neither side reports — a green here is not evidence. The field says which pair disagrees and names the exact fix: run \`${CLI} stop\` and retry when the DAEMON is stale, or tell the human to install the matching SDK and restart their dev server when the PAGE is. Do that first, then redo whatever you were verifying.
45
+ - **If a tool result carries \`update_available\`, say so.** Reticle checked npm and a newer version exists. Tell the human the version numbers and offer to run \`${CLI} update\` — between tasks, never mid-verification, because it restarts the daemon and drops the session. Finish what you are doing first.
46
+ - **Wish Reticle could do something? Ask for it with \`reticle_feedback\`.** Reticle is built for agents, so what you find awkward or missing is what decides the roadmap — and you are the only one who can report it, because that thought disappears when your context does. Put the report itself in \`text\` and pick a \`kind\` — both are required: \`feature_request\` for something that does not exist, \`improvement\` for something that works but costs too many calls, \`bug\` for a tool that returned something wrong, \`gap\` for something Reticle could not observe at all, \`ambiguity\` for a verdict you could not act on. Say what you are actually trying to achieve (\`need\`) and how you work around it today (\`currentApproach\`), and set \`model\` to the model you are running. File it the moment you notice, then carry on — nothing is too minor.
47
+ - **If Reticle itself fails you, tell its maintainers with \`reticle_feedback\`.** A tool that returned something wrong, a thing Reticle could not observe at all, or a verdict you could not act on — file it as \`text\` — a root-cause analysis with the call trace — under \`kind: bug\` (wrong result), \`gap\` (could not observe it) or \`ambiguity\` (undecidable verdict), then carry on with the task. Working around a broken tool in silence is how the same defect survives. This is about defects in *Reticle*; a bug you find in *this app* is Reticle working, and belongs in your report to the user instead.`;
34
48
  export const AgentRuleStatus = {
35
49
  APPLY: 'apply',
36
50
  ALREADY: 'already',
@@ -46,15 +60,39 @@ export function markedBlock() {
46
60
  */
47
61
  export function mergeMarkedInstruction(existing) {
48
62
  if (existing !== null && existing !== undefined && existing.includes(RULE_BEGIN)) {
49
- return { status: AgentRuleStatus.ALREADY, content: existing };
63
+ // A block that can never be updated is not managed. Returning ALREADY on sight of the marker
64
+ // meant a project that ran init once kept that release's rule text forever, so every improvement
65
+ // reached only new projects — and a rule about a field introduced later (`version_skew`) could
66
+ // never arrive at all. The markers promise the inside is ours; idempotence comes from comparing
67
+ // CONTENT instead of presence.
68
+ return refreshMarkedBlock(existing);
50
69
  }
51
70
  const block = markedBlock();
52
- if (existing === null || existing === undefined || existing.trim().length === 0) {
71
+ if (null === existing || existing === undefined || 0 === existing.trim().length) {
53
72
  return { status: AgentRuleStatus.APPLY, content: block };
54
73
  }
55
74
  const separator = existing.endsWith('\n') ? '\n' : '\n\n';
56
75
  return { status: AgentRuleStatus.APPLY, content: `${existing}${separator}${block}` };
57
76
  }
77
+ /**
78
+ * Swap the managed block for the current one, leaving every character outside the markers alone.
79
+ *
80
+ * A malformed file — a begin marker with no end — is left untouched and reported ALREADY. Rewriting
81
+ * from a marker to end-of-file would eat whatever the human wrote after it, and silently destroying
82
+ * someone's instruction file is far worse than a stale rule.
83
+ */
84
+ function refreshMarkedBlock(existing) {
85
+ const start = existing.indexOf(RULE_BEGIN);
86
+ const endAt = existing.indexOf(RULE_END, start);
87
+ if (-1 === endAt)
88
+ return { status: AgentRuleStatus.ALREADY, content: existing };
89
+ const current = existing.slice(start, endAt + RULE_END.length);
90
+ const wanted = markedBlock().trimEnd();
91
+ if (current === wanted)
92
+ return { status: AgentRuleStatus.ALREADY, content: existing };
93
+ const content = `${existing.slice(0, start)}${wanted}${existing.slice(endAt + RULE_END.length)}`;
94
+ return { status: AgentRuleStatus.APPLY, content };
95
+ }
58
96
  /** The Cursor rule file (.mdc): `alwaysApply` keeps the rule in every turn's context. */
59
97
  export function cursorRuleFile() {
60
98
  return `---
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Which app in a workspace `reticle init` should wire, when the caller says.
3
+ *
4
+ * Refusing to guess between several apps is right — instrumenting one silently would leave the rest
5
+ * unverified while reporting success. But "re-run inside the one you want" is not an answer a
6
+ * script, a CI step, or an agent that cannot change directory can act on, so the refusal was a dead
7
+ * end for exactly the callers most likely to hit it.
8
+ *
9
+ * A name that is not one of the discovered apps is refused rather than trusted: it is almost always
10
+ * a typo or a stale path, and wiring a directory that is not an app writes files nothing compiles.
11
+ */
12
+ type AppChoice = {
13
+ ok: true;
14
+ app: string | undefined;
15
+ } | {
16
+ ok: false;
17
+ message: string;
18
+ };
19
+ export declare function chooseWorkspaceApp(requested: string | undefined, apps: readonly string[]): AppChoice;
20
+ export {};
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Which app in a workspace `reticle init` should wire, when the caller says.
3
+ *
4
+ * Refusing to guess between several apps is right — instrumenting one silently would leave the rest
5
+ * unverified while reporting success. But "re-run inside the one you want" is not an answer a
6
+ * script, a CI step, or an agent that cannot change directory can act on, so the refusal was a dead
7
+ * end for exactly the callers most likely to hit it.
8
+ *
9
+ * A name that is not one of the discovered apps is refused rather than trusted: it is almost always
10
+ * a typo or a stale path, and wiring a directory that is not an app writes files nothing compiles.
11
+ */
12
+ export function chooseWorkspaceApp(requested, apps) {
13
+ if (requested === undefined || '' === requested)
14
+ return { ok: true, app: undefined };
15
+ // Tab-completion adds a trailing slash; the discovered names never carry one.
16
+ const wanted = requested.replace(/\/+$/, '');
17
+ if (apps.includes(wanted))
18
+ return { ok: true, app: wanted };
19
+ return {
20
+ ok: false,
21
+ message: 0 === apps.length
22
+ ? `--app ${requested} was given, but no app was found in this workspace`
23
+ : `--app ${requested} is not one of the apps found here: ${apps.join(', ')}`,
24
+ };
25
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Which Astro file owns the document, and therefore where the SDK script belongs.
3
+ *
4
+ * The previous rule was "exactly one `.astro` file in `src/layouts/`". Measured on two real Astro
5
+ * projects, it fired on neither: one has no `src/layouts/` at all and renders from
6
+ * `src/pages/index.astro`; the other has three files there, two of which are partials. Both got a
7
+ * printed recipe instead of a working install.
8
+ *
9
+ * A file COUNT cannot tell a layout from a partial, and a directory NAME cannot tell you where the
10
+ * document shell is. `</body>` can — the file that closes the body is the one every page renders
11
+ * through, and the only place a script reaches the whole app.
12
+ *
13
+ * Ambiguity is still refused rather than guessed. Two document shells means two entry points, and
14
+ * silently instrumenting one of them would leave half the app unverified while reporting success.
15
+ */
16
+ interface AstroCandidate {
17
+ path: string;
18
+ source: string;
19
+ }
20
+ export declare function pickAstroHost(candidates: readonly AstroCandidate[]): AstroCandidate | undefined;
21
+ export {};
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Which Astro file owns the document, and therefore where the SDK script belongs.
3
+ *
4
+ * The previous rule was "exactly one `.astro` file in `src/layouts/`". Measured on two real Astro
5
+ * projects, it fired on neither: one has no `src/layouts/` at all and renders from
6
+ * `src/pages/index.astro`; the other has three files there, two of which are partials. Both got a
7
+ * printed recipe instead of a working install.
8
+ *
9
+ * A file COUNT cannot tell a layout from a partial, and a directory NAME cannot tell you where the
10
+ * document shell is. `</body>` can — the file that closes the body is the one every page renders
11
+ * through, and the only place a script reaches the whole app.
12
+ *
13
+ * Ambiguity is still refused rather than guessed. Two document shells means two entry points, and
14
+ * silently instrumenting one of them would leave half the app unverified while reporting success.
15
+ */
16
+ /** Tolerant of whitespace and casing: `</body>`, `</BODY >`, `</body\n>`. */
17
+ const CLOSES_BODY = /<\/\s*body\s*>/i;
18
+ /** Layout files win over pages: a page shell is one route, a layout shell is what routes inherit. */
19
+ const LAYOUT_DIR = 'layouts/';
20
+ export function pickAstroHost(candidates) {
21
+ const owners = candidates.filter((c) => CLOSES_BODY.test(c.source));
22
+ if (0 === owners.length)
23
+ return undefined;
24
+ const layouts = owners.filter((c) => c.path.includes(LAYOUT_DIR));
25
+ const preferred = layouts.length > 0 ? layouts : owners;
26
+ return 1 === preferred.length ? preferred[0] : undefined;
27
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Pure, conservative patchers for an Astro app: the config gets the token/root `define` and the
3
+ * raised build target, one layout gets the dev-only connect `<script>`.
4
+ *
5
+ * Astro was the last gated framework where `init` printed a correct recipe and applied none of it —
6
+ * the only ⚠ left on a supported stack, and the user's first session was two hand-copied snippets
7
+ * away. Astro is Vite-based but renders its own HTML, so the Vite plugin's injection never fires and
8
+ * there is no entry module to wire: the connect has to live in a page or layout `<script>`, and the
9
+ * token has to be inlined by the config because nothing else is in the page's path to inject it.
10
+ *
11
+ * Both patchers bail to `manual` (the printed recipe) on any shape they do not fully recognise.
12
+ * Half-editing a build config is worse than a documented manual step.
13
+ */
14
+ import { type SourcePatch } from './patch-kind.js';
15
+ export declare function patchAstroConfig(source: string): SourcePatch;
16
+ export declare function patchAstroLayout(source: string, port: number | undefined, projectId: string | undefined): SourcePatch;
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Pure, conservative patchers for an Astro app: the config gets the token/root `define` and the
3
+ * raised build target, one layout gets the dev-only connect `<script>`.
4
+ *
5
+ * Astro was the last gated framework where `init` printed a correct recipe and applied none of it —
6
+ * the only ⚠ left on a supported stack, and the user's first session was two hand-copied snippets
7
+ * away. Astro is Vite-based but renders its own HTML, so the Vite plugin's injection never fires and
8
+ * there is no entry module to wire: the connect has to live in a page or layout `<script>`, and the
9
+ * token has to be inlined by the config because nothing else is in the page's path to inject it.
10
+ *
11
+ * Both patchers bail to `manual` (the printed recipe) on any shape they do not fully recognise.
12
+ * Half-editing a build config is worse than a documented manual step.
13
+ */
14
+ import { RETICLE_DEFAULT_PORT, bridgeWsUrl } from '@reticlehq/core';
15
+ import { PatchKind } from './patch-kind.js';
16
+ /** Present in a patched config AND in a hand-followed recipe — so both count as already wired. */
17
+ const CONFIG_MARKER = '__RETICLE_TOKEN__';
18
+ /** Present in a patched layout and in the printed recipe's script. */
19
+ const LAYOUT_MARKER = 'reticle.connect';
20
+ /**
21
+ * The SDK, declared so Vite pre-bundles it BEFORE the first page load.
22
+ *
23
+ * The connect script does `await import('@reticlehq/react')`. Undeclared, Vite meets that import
24
+ * mid-load, pre-bundles it, and the hashed `/node_modules/.vite/deps/@reticlehq_react.js?v=…` URL
25
+ * the browser already requested stops existing — the import rejects with "Failed to fetch
26
+ * dynamically imported module", connect() never runs, and the page looks entirely normal. Measured
27
+ * on astro-nanostores, intermittent on whether the dep cache was warm. The Vite plugin has declared
28
+ * the SDK for this exact reason since the bug was first found on React; Astro is hand-patched and
29
+ * never got it.
30
+ */
31
+ const SDK_INCLUDE_LITERAL = `'@reticlehq/react'`;
32
+ /**
33
+ * An `include:` the app already declared — ours joins that array instead of adding a second key.
34
+ * Deliberately not anchored to a newline: `optimizeDeps: { include: ['x'] }` on one line is the
35
+ * common way to write it, and requiring a line break made the merge miss it and duplicate the key.
36
+ */
37
+ const EXISTING_INCLUDE = /(\s*include\s*:\s*\[)/;
38
+ const DEFINE_CONFIG = /defineConfig\s*\(\s*\{/;
39
+ /** A `vite:` key whose value is an object LITERAL — the one shape we can merge into safely. */
40
+ const VITE_OBJECT_KEY = /(^\s*vite\s*:\s*\{)/m;
41
+ /** Any `vite:` key at all. One that is not an object literal (a spread, an imported config) is ours to refuse. */
42
+ const ANY_VITE_KEY = /^\s*vite\s*:/m;
43
+ const BODY_CLOSE = '</body>';
44
+ const HELPER = `
45
+ function reticleToken() {
46
+ const dir = process.env['RETICLE_PAIRING_TOKEN_DIR'] || join(homedir(), '.reticle');
47
+ try { return readFileSync(join(dir, 'pairing-token'), 'utf8').trim(); } catch { return ''; }
48
+ }
49
+ `;
50
+ const HELPER_IMPORTS = `import { readFileSync } from 'node:fs';
51
+ import { homedir } from 'node:os';
52
+ import { join } from 'node:path';
53
+ `;
54
+ /**
55
+ * The keys we add inside `vite:`, each with what it must contain.
56
+ *
57
+ * Kept as a list rather than one blob because a config that already sets one of these must be merged
58
+ * INTO, not duplicated: the real astro-nanostores config has `build: { chunkSizeWarningLimit }`, and
59
+ * inserting our own `build` beside it gave the object two — where the last one wins, so
60
+ * `target: 'es2022'` was silently discarded while init reported success. Losing that target is not
61
+ * cosmetic: Astro's default down-levels the modern SDK bundle and dies on a destructuring transform.
62
+ */
63
+ const VITE_KEYS = [
64
+ { key: 'build', inner: `\n target: 'es2022',` },
65
+ {
66
+ key: 'optimizeDeps',
67
+ inner: `\n include: [${SDK_INCLUDE_LITERAL}],\n esbuildOptions: { target: 'es2022' },`,
68
+ },
69
+ {
70
+ key: 'define',
71
+ inner: `\n __RETICLE_TOKEN__: JSON.stringify(reticleToken()),\n __RETICLE_ROOT__: JSON.stringify(process.cwd()),`,
72
+ },
73
+ ];
74
+ /** Whole-key form, for a `vite:` block that does not have the key at all. */
75
+ const WHOLE = {
76
+ build: `\n build: { target: 'es2022' },`,
77
+ optimizeDeps: `\n optimizeDeps: { include: [${SDK_INCLUDE_LITERAL}], esbuildOptions: { target: 'es2022' } },`,
78
+ define: `\n define: {\n __RETICLE_TOKEN__: JSON.stringify(reticleToken()),\n __RETICLE_ROOT__: JSON.stringify(process.cwd()),\n },`,
79
+ };
80
+ /**
81
+ * Add our keys to an existing `vite: { ... }` block, merging into any the app already set.
82
+ *
83
+ * Returns null when a colliding key is not an object literal (`build: sharedConfig`) — there is no
84
+ * brace to merge into, and duplicating or replacing it would corrupt someone's build.
85
+ */
86
+ function mergeIntoViteBlock(source, braceAt) {
87
+ let out = source;
88
+ for (const { key, inner } of VITE_KEYS) {
89
+ // Scoped to the vite block by searching from its opening brace: a `build:` under `markdown:` or
90
+ // at the top level is somebody else's key and must not be touched.
91
+ const existing = new RegExp(`(\\n\\s*${key}\\s*:\\s*)([\\{A-Za-z_])`).exec(out.slice(braceAt));
92
+ if (existing?.index === undefined) {
93
+ out = `${out.slice(0, braceAt)}${WHOLE[key] ?? ''}${out.slice(braceAt)}`;
94
+ continue;
95
+ }
96
+ // The character after the colon decides: `{` is a literal we can open; anything else is a
97
+ // reference we must not touch.
98
+ if (existing[2] !== '{')
99
+ return null;
100
+ const at = braceAt + existing.index + existing[0].length; // just past the `{`
101
+ // An `include:` the app already wrote is an ARRAY, and a second `include:` key in the same
102
+ // object literal is not a merge — the last one wins and one of the two silently disappears.
103
+ // That is how `build.target` was lost while init still reported success. Join their array.
104
+ // Scoped to THIS block's braces: an `include:` under a later key is somebody else's array, and
105
+ // appending the SDK to it would both miss the target and edit a config we were not asked to.
106
+ const own = 'optimizeDeps' === key ? EXISTING_INCLUDE.exec(blockAfter(out, at)) : null;
107
+ if (own?.index === undefined) {
108
+ out = insertAt(out, at, inner);
109
+ continue;
110
+ }
111
+ // Their array first, then the rest of our keys at the block's start — inserting at the LOWER
112
+ // offset second keeps the first offset valid.
113
+ out = insertAt(out, at + own.index + own[0].length, `${SDK_INCLUDE_LITERAL}, `);
114
+ out = insertAt(out, at, withoutInclude(inner));
115
+ }
116
+ return out;
117
+ }
118
+ function insertAt(source, at, text) {
119
+ return `${source.slice(0, at)}${text}${source.slice(at)}`;
120
+ }
121
+ /**
122
+ * The text from just inside an opening brace to its match — a brace count, not a parser.
123
+ *
124
+ * Enough for the one question asked of it (does THIS object literal already declare `include`), and
125
+ * honest about its limit: a `{` or `}` inside a string literal in someone's Vite config would fool
126
+ * it. That only ever shortens or lengthens the window we search for `include:` in, so the worst case
127
+ * is the same duplicate-key merge we would have done before, never a corrupted file.
128
+ */
129
+ function blockAfter(source, at) {
130
+ let depth = 1;
131
+ for (let i = at; i < source.length; i++) {
132
+ if ('{' === source[i])
133
+ depth += 1;
134
+ else if ('}' === source[i]) {
135
+ depth -= 1;
136
+ if (0 === depth)
137
+ return source.slice(at, i);
138
+ }
139
+ }
140
+ return source.slice(at);
141
+ }
142
+ /** The optimizeDeps inner minus our `include:` line, for when the app already has one to join. */
143
+ function withoutInclude(inner) {
144
+ return inner.replace(`\n include: [${SDK_INCLUDE_LITERAL}],`, '');
145
+ }
146
+ /**
147
+ * The `vite:` block Astro needs. `build.target` is raised because Astro's default down-levels the
148
+ * modern SDK bundle and dies on a destructuring transform; `__RETICLE_ROOT__` is defined because
149
+ * without it every source pointer comes back as an absolute path from the machine that ran `init`.
150
+ */
151
+ const VITE_BLOCK = ` vite: {
152
+ build: { target: 'es2022' },
153
+ optimizeDeps: { include: [${SDK_INCLUDE_LITERAL}], esbuildOptions: { target: 'es2022' } },
154
+ define: {
155
+ __RETICLE_TOKEN__: JSON.stringify(reticleToken()),
156
+ __RETICLE_ROOT__: JSON.stringify(process.cwd()),
157
+ },
158
+ },
159
+ `;
160
+ export function patchAstroConfig(source) {
161
+ if (source.includes(CONFIG_MARKER))
162
+ return { kind: PatchKind.ALREADY };
163
+ // A `vite: { ... }` block is an object literal, so our keys can go straight after the brace and
164
+ // whatever is already in there is untouched. Refusing outright was the only genuine install defect
165
+ // left in the gate: the config bailed while the LAYOUT patch applied anyway, leaving an app with a
166
+ // connect snippet, no inlined token and no raised build target — which cannot connect, reported as
167
+ // one OK step and one warning.
168
+ const viteObject = VITE_OBJECT_KEY.exec(source);
169
+ if (viteObject?.index !== undefined) {
170
+ const at = viteObject.index + viteObject[0].length;
171
+ const merged = mergeIntoViteBlock(source, at);
172
+ if (merged !== null) {
173
+ return {
174
+ kind: PatchKind.APPLY,
175
+ code: `${HELPER_IMPORTS}${merged.trimStart()}\n${HELPER}`.trimEnd() + '\n',
176
+ };
177
+ }
178
+ return {
179
+ kind: PatchKind.MANUAL,
180
+ reason: 'this config sets a `vite` key Reticle needs (build / optimizeDeps / define) to something ' +
181
+ 'other than an object literal — merging into it is your call, not a text edit Reticle should make',
182
+ };
183
+ }
184
+ if (ANY_VITE_KEY.test(source)) {
185
+ // `vite: sharedConfig` — not an object literal, so there is no brace to merge into and guessing
186
+ // would corrupt a build config. The recipe is the honest answer here.
187
+ return {
188
+ kind: PatchKind.MANUAL,
189
+ reason: 'this config sets `vite:` to something other than an object literal — merging into it is your call, not a text edit Reticle should make',
190
+ };
191
+ }
192
+ const opening = DEFINE_CONFIG.exec(source);
193
+ if (opening?.index === undefined) {
194
+ return {
195
+ kind: PatchKind.MANUAL,
196
+ reason: "couldn't find a `defineConfig({ ... })` call to extend",
197
+ };
198
+ }
199
+ const at = opening.index + opening[0].length;
200
+ const withBlock = `${source.slice(0, at)}\n${VITE_BLOCK}${source.slice(at)}`;
201
+ return {
202
+ kind: PatchKind.APPLY,
203
+ code: `${HELPER_IMPORTS}${withBlock.trimStart()}\n${HELPER}`.trimEnd() + '\n',
204
+ };
205
+ }
206
+ /** The dev-only connect that goes inside the layout's `<body>`. */
207
+ function astroConnectScript(port, projectId) {
208
+ const url = port !== undefined && port !== RETICLE_DEFAULT_PORT
209
+ ? `\n url: '${bridgeWsUrl(port)}',`
210
+ : '';
211
+ const id = projectId !== undefined && projectId.length > 0 ? `\n projectId: '${projectId}',` : '';
212
+ return ` <script>
213
+ if (import.meta.env.DEV) {
214
+ const token = typeof __RETICLE_TOKEN__ !== 'undefined' ? __RETICLE_TOKEN__ : '';
215
+ const root = typeof __RETICLE_ROOT__ !== 'undefined' ? __RETICLE_ROOT__ : '';
216
+ const { reticle, install } = await import('@reticlehq/react');
217
+ install();
218
+ reticle.connect({${id}${url}
219
+ ...(token.length > 0 ? { token } : {}),
220
+ ...(root.length > 0 ? { root } : {}),
221
+ });
222
+ }
223
+ </script>
224
+ `;
225
+ }
226
+ export function patchAstroLayout(source, port, projectId) {
227
+ if (source.includes(LAYOUT_MARKER))
228
+ return { kind: PatchKind.ALREADY };
229
+ const at = source.lastIndexOf(BODY_CLOSE);
230
+ if (at < 0) {
231
+ return {
232
+ kind: PatchKind.MANUAL,
233
+ reason: "couldn't find a `</body>` to place the connect script before",
234
+ };
235
+ }
236
+ return {
237
+ kind: PatchKind.APPLY,
238
+ code: `${source.slice(0, at)}${astroConnectScript(port, projectId)}${source.slice(at)}`,
239
+ };
240
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * What the app can be driven by: its `data-testid` values, and which state library holds its truth.
3
+ *
4
+ * `init` wired none of this, so every app came up with `hasCapabilities: false`, empty capabilities,
5
+ * and a `reticle_state` holding nothing but `__reticle_renders` — the state-truth read that SKILL.md
6
+ * calls the highest-value line was unavailable on every app out of the box. Measured across six.
7
+ *
8
+ * Testids are scanned rather than asked for. Stores are NAMED but not wired: detecting that an app
9
+ * depends on zustand is easy, knowing which module exports the store instance is not, and generating
10
+ * a wrong import would break the dev module that everything else now hangs off.
11
+ */
12
+ /** Cap the generated list. A capabilities block is a hint for an agent, not an inventory. */
13
+ export declare const MAX_TESTIDS = 60;
14
+ /** Every distinct `data-testid` literal in the given sources, in first-seen order. */
15
+ export declare function scanTestids(sources: readonly string[]): string[];
16
+ /** The store libraries this app depends on, as ready-to-uncomment registration lines. */
17
+ export declare function storeHints(deps: ReadonlySet<string>): string[];