@reticlehq/server 3.3.0 → 3.4.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 (299) hide show
  1. package/LICENSE-ENTERPRISE +1 -1
  2. package/README.md +3 -3
  3. package/SKILL.md +12 -14
  4. package/dist/chromium-launch-options.d.ts +4 -3
  5. package/dist/chromium-launch-options.js +6 -2
  6. package/dist/command/cli/adhoc-suite.d.ts +22 -0
  7. package/dist/command/cli/adhoc-suite.js +90 -19
  8. package/dist/command/cli/adhoc-verdict.d.ts +35 -0
  9. package/dist/command/cli/adhoc-verdict.js +90 -3
  10. package/dist/command/cli/answers/gate-hook-message.d.ts +6 -0
  11. package/dist/command/cli/answers/gate-hook-message.js +6 -0
  12. package/dist/command/cli/auth/cloud-keystore.d.ts +0 -2
  13. package/dist/command/cli/auth/cloud-keystore.js +0 -3
  14. package/dist/command/cli/auth/cloud-session.d.ts +4 -0
  15. package/dist/command/cli/auth/cloud-session.js +2 -0
  16. package/dist/command/cli/cli-doctor.js +7 -5
  17. package/dist/command/cli/cli-flow-commands.d.ts +3 -6
  18. package/dist/command/cli/cli-flow-commands.js +67 -4
  19. package/dist/command/cli/cli-parse.d.ts +8 -1
  20. package/dist/command/cli/cli-parse.js +21 -3
  21. package/dist/command/cli/cli-update-commands.js +8 -1
  22. package/dist/command/cli/cli-verify.d.ts +24 -3
  23. package/dist/command/cli/cli-verify.js +54 -11
  24. package/dist/command/cli/cloud-cli.js +5 -2
  25. package/dist/command/cli/cloud-kit.js +3 -5
  26. package/dist/command/cli/cloud-login.js +21 -3
  27. package/dist/command/cli/demo/demo-page.js +6 -3
  28. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +5 -0
  29. package/dist/command/cli/doctor/browser/chromium-hint.js +5 -0
  30. package/dist/command/cli/launch/cli-launch.d.ts +8 -0
  31. package/dist/command/cli/launch/cli-launch.js +21 -9
  32. package/dist/command/cli/launch/sdk-version-match.d.ts +84 -0
  33. package/dist/command/cli/launch/sdk-version-match.js +206 -0
  34. package/dist/command/cli/lifecycle/daemon-lifecycle.js +2 -16
  35. package/dist/command/cli/ports/resolve/cli-port.d.ts +25 -0
  36. package/dist/command/cli/ports/resolve/cli-port.js +125 -15
  37. package/dist/command/cli/setup-mcp-cli.js +13 -13
  38. package/dist/command/cli/tutorial.d.ts +2 -2
  39. package/dist/command/cli/tutorial.js +21 -7
  40. package/dist/command/cli.js +62 -10
  41. package/dist/command/daemon/binding/daemon-status-probe.d.ts +14 -2
  42. package/dist/command/daemon/binding/daemon-status-probe.js +34 -6
  43. package/dist/command/daemon/daemon-resolve.d.ts +9 -0
  44. package/dist/command/daemon/daemon-resolve.js +1 -1
  45. package/dist/command/daemon/lifetime/daemon-usefulness.d.ts +7 -1
  46. package/dist/command/daemon/lifetime/daemon-usefulness.js +30 -7
  47. package/dist/command/dev/stale-issue-guard.d.ts +3 -4
  48. package/dist/command/dev/stale-issue-guard.js +8 -8
  49. package/dist/command/setup/bringup/connect-lease.d.ts +17 -0
  50. package/dist/command/setup/bringup/connect-lease.js +38 -0
  51. package/dist/command/setup/bringup/dev-server-files.d.ts +68 -0
  52. package/dist/command/setup/bringup/dev-server-files.js +131 -0
  53. package/dist/command/setup/bringup/dev-server-log-cap.d.ts +35 -0
  54. package/dist/command/setup/bringup/dev-server-log-cap.js +121 -0
  55. package/dist/command/setup/bringup/ensure-daemon.d.ts +65 -4
  56. package/dist/command/setup/bringup/ensure-daemon.js +135 -16
  57. package/dist/command/setup/bringup/static-page-server.d.ts +35 -0
  58. package/dist/command/setup/bringup/static-page-server.js +151 -0
  59. package/dist/command/setup/desktop-shape.d.ts +4 -0
  60. package/dist/command/setup/desktop-shape.js +18 -2
  61. package/dist/command/setup/init/init-port.d.ts +15 -0
  62. package/dist/command/setup/init/init-port.js +44 -0
  63. package/dist/command/setup/init/init-runtime.d.ts +1 -1
  64. package/dist/command/setup/init/init-runtime.js +39 -2
  65. package/dist/command/setup/node-effects.d.ts +27 -14
  66. package/dist/command/setup/node-effects.js +80 -44
  67. package/dist/command/setup/probe/loopback-probe-urls.d.ts +7 -0
  68. package/dist/command/setup/probe/loopback-probe-urls.js +40 -0
  69. package/dist/command/setup/probe/page-probe.d.ts +13 -2
  70. package/dist/command/setup/probe/page-probe.js +18 -3
  71. package/dist/command/setup/run-setup.d.ts +26 -1
  72. package/dist/command/setup/run-setup.js +96 -13
  73. package/dist/command/setup/session-pick.d.ts +13 -0
  74. package/dist/command/setup/session-pick.js +20 -0
  75. package/dist/command/setup/setup-command.d.ts +2 -14
  76. package/dist/command/setup/setup-command.js +105 -31
  77. package/dist/command/setup/setup-mcp.js +3 -3
  78. package/dist/command/setup/terminal/wait-progress.d.ts +8 -0
  79. package/dist/command/setup/terminal/wait-progress.js +17 -0
  80. package/dist/features/crawl/crawl-empty.js +1 -1
  81. package/dist/features/crawl/crawl-tools.d.ts +6 -0
  82. package/dist/features/crawl/crawl-tools.js +57 -0
  83. package/dist/features/crawl/crawl.js +171 -157
  84. package/dist/features/exhaust/app-coverage.d.ts +33 -0
  85. package/dist/features/exhaust/app-coverage.js +31 -0
  86. package/dist/features/exhaust/code-coverage.d.ts +47 -0
  87. package/dist/features/exhaust/code-coverage.js +77 -0
  88. package/dist/features/exhaust/exhaust-run.d.ts +33 -0
  89. package/dist/features/exhaust/exhaust-run.js +82 -0
  90. package/dist/features/exhaust/explorer.d.ts +103 -0
  91. package/dist/features/exhaust/explorer.js +195 -0
  92. package/dist/features/exhaust/failure-branches.d.ts +33 -0
  93. package/dist/features/exhaust/failure-branches.js +92 -0
  94. package/dist/features/exhaust/ledger.d.ts +202 -0
  95. package/dist/features/exhaust/ledger.js +252 -0
  96. package/dist/features/exhaust/session-fold.d.ts +31 -0
  97. package/dist/features/exhaust/session-fold.js +79 -0
  98. package/dist/features/exhaust/session-port.d.ts +36 -0
  99. package/dist/features/exhaust/session-port.js +102 -0
  100. package/dist/features/harness/drive-report.d.ts +0 -9
  101. package/dist/features/harness/drive-report.js +18 -0
  102. package/dist/features/harness/driver.js +4 -7
  103. package/dist/features/harness/fill-values.d.ts +6 -0
  104. package/dist/features/harness/fill-values.js +3 -0
  105. package/dist/features/harness/jev-driver.js +30 -13
  106. package/dist/features/harness/openai-driver.js +4 -7
  107. package/dist/features/harness/platform-config.js +5 -6
  108. package/dist/index.d.ts +9 -0
  109. package/dist/index.js +40 -11
  110. package/dist/judgement/domain/domain-model.js +3 -1
  111. package/dist/judgement/oracles/self-instrument.js +4 -2
  112. package/dist/judgement/runs/artifact/replay-mapping.d.ts +2 -2
  113. package/dist/judgement/runs/artifact/replay-mapping.js +44 -9
  114. package/dist/judgement/runs/verification-sync.d.ts +3 -1
  115. package/dist/judgement/runs/verification-sync.js +1 -1
  116. package/dist/language/flows/change/affected.d.ts +9 -0
  117. package/dist/language/flows/change/affected.js +35 -0
  118. package/dist/language/flows/change/gate.d.ts +20 -0
  119. package/dist/language/flows/change/gate.js +12 -1
  120. package/dist/language/flows/flow-author.d.ts +7 -0
  121. package/dist/language/flows/flow-author.js +19 -0
  122. package/dist/language/flows/flow-expect-grammar.js +1 -1
  123. package/dist/language/flows/flow-journey.d.ts +54 -0
  124. package/dist/language/flows/flow-journey.js +119 -0
  125. package/dist/language/flows/flow-replay-run.d.ts +2 -3
  126. package/dist/language/flows/flow-replay-run.js +13 -47
  127. package/dist/language/flows/flow-replay.d.ts +22 -8
  128. package/dist/language/flows/flow-replay.js +55 -9
  129. package/dist/language/flows/flow-result.d.ts +2 -0
  130. package/dist/language/flows/flow-result.js +25 -0
  131. package/dist/language/flows/flow-tools.js +17 -5
  132. package/dist/language/flows/flows.d.ts +10 -1
  133. package/dist/language/flows/flows.js +90 -14
  134. package/dist/language/flows/recording/replay-halt.js +3 -1
  135. package/dist/language/flows/recording/tape/recordings.d.ts +30 -1
  136. package/dist/language/flows/recording/tape/recordings.js +55 -1
  137. package/dist/language/flows/replay.d.ts +19 -0
  138. package/dist/language/flows/replay.js +42 -6
  139. package/dist/language/flows/verify-change-tools.js +5 -2
  140. package/dist/launch-chromium.d.ts +80 -0
  141. package/dist/launch-chromium.js +218 -0
  142. package/dist/memory/cloud/cloud-config.d.ts +11 -2
  143. package/dist/memory/cloud/cloud-config.js +47 -7
  144. package/dist/memory/cloud/cloud-sync.d.ts +4 -1
  145. package/dist/memory/cloud/cloud-sync.js +6 -9
  146. package/dist/memory/cloud/harness-offer.js +5 -6
  147. package/dist/memory/cloud/harness-switch.js +5 -6
  148. package/dist/memory/cloud/sync-cycle.d.ts +68 -3
  149. package/dist/memory/cloud/sync-cycle.js +298 -55
  150. package/dist/memory/cloud/sync-daemon.js +24 -20
  151. package/dist/memory/cloud/sync-disk.js +16 -2
  152. package/dist/memory/intent/intent-shard.d.ts +10 -32
  153. package/dist/memory/intent/intent-shard.js +0 -10
  154. package/dist/memory/journal/drive-flow.d.ts +18 -13
  155. package/dist/memory/journal/drive-flow.js +45 -37
  156. package/dist/memory/journal/on-disk/workspace-tiers.js +2 -0
  157. package/dist/memory/journal/session-end.d.ts +9 -1
  158. package/dist/memory/journal/session-end.js +24 -12
  159. package/dist/memory/project/dir/reticle-dir.d.ts +2 -0
  160. package/dist/memory/project/dir/reticle-dir.js +1 -0
  161. package/dist/memory/project/project-tools.js +3 -1
  162. package/dist/portal/bridge/auth-failure-reason.d.ts +1 -1
  163. package/dist/portal/bridge/auth-failure-reason.js +4 -2
  164. package/dist/portal/bridge/bridge.d.ts +8 -0
  165. package/dist/portal/bridge/bridge.js +43 -2
  166. package/dist/portal/input/js-coverage.d.ts +25 -0
  167. package/dist/portal/input/js-coverage.js +30 -0
  168. package/dist/portal/input/real-input.d.ts +11 -0
  169. package/dist/portal/input/real-input.js +29 -4
  170. package/dist/portal/pool/playwright-launcher.d.ts +6 -0
  171. package/dist/portal/pool/playwright-launcher.js +20 -13
  172. package/dist/portal/session/dev-server/dev-command.d.ts +2 -2
  173. package/dist/portal/session/dev-server/dev-command.js +30 -12
  174. package/dist/portal/session/dev-server/dev-server-probe.js +17 -1
  175. package/dist/portal/session/dev-server/route-status-probe.d.ts +24 -0
  176. package/dist/portal/session/dev-server/route-status-probe.js +57 -0
  177. package/dist/portal/session/facts/observed-state.d.ts +11 -0
  178. package/dist/portal/session/facts/observed-state.js +34 -0
  179. package/dist/portal/session/no-session-diagnosis.d.ts +29 -0
  180. package/dist/portal/session/no-session-diagnosis.js +62 -5
  181. package/dist/portal/session/no-session-next-action.d.ts +13 -0
  182. package/dist/portal/session/no-session-next-action.js +48 -9
  183. package/dist/portal/session/no-session-watch.d.ts +18 -0
  184. package/dist/portal/session/no-session-watch.js +88 -2
  185. package/dist/portal/session/session-health.js +3 -2
  186. package/dist/portal/session/session-manager.d.ts +20 -6
  187. package/dist/portal/session/session-manager.js +19 -3
  188. package/dist/portal/session/session-reaper.d.ts +28 -0
  189. package/dist/portal/session/session-reaper.js +49 -1
  190. package/dist/portal/session/session.d.ts +5 -0
  191. package/dist/portal/session/session.js +15 -19
  192. package/dist/surface/http-server.d.ts +2 -1
  193. package/dist/surface/http-server.js +4 -2
  194. package/dist/surface/mcp/mcp-proxy.d.ts +2 -2
  195. package/dist/surface/mcp/mcp-proxy.js +2 -2
  196. package/dist/surface/mcp/mcp.js +4 -2
  197. package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +13 -1
  198. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +27 -2
  199. package/dist/surface/mcp/server-instructions.js +9 -7
  200. package/dist/surface/tools/act/after-match.d.ts +17 -0
  201. package/dist/surface/tools/act/after-match.js +30 -0
  202. package/dist/surface/tools/act/durable.d.ts +69 -0
  203. package/dist/surface/tools/act/durable.js +139 -0
  204. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +13 -6
  205. package/dist/surface/tools/act/navigation/navigate-arrival.js +47 -10
  206. package/dist/surface/tools/act/navigation/navigate-result.d.ts +1 -12
  207. package/dist/surface/tools/act/navigation/navigate-result.js +21 -3
  208. package/dist/surface/tools/act/proved-controls.d.ts +9 -0
  209. package/dist/surface/tools/act/proved-controls.js +11 -0
  210. package/dist/surface/tools/act/sequence-grade.d.ts +5 -0
  211. package/dist/surface/tools/act/sequence-grade.js +32 -1
  212. package/dist/surface/tools/act-capsule.d.ts +7 -0
  213. package/dist/surface/tools/act-capsule.js +46 -1
  214. package/dist/surface/tools/act-sequence-tool.js +25 -8
  215. package/dist/surface/tools/act-tools.js +26 -50
  216. package/dist/surface/tools/assert/assert-grade.js +12 -1
  217. package/dist/surface/tools/assert/assert-verdict.js +2 -2
  218. package/dist/surface/tools/browser-tools.js +14 -2
  219. package/dist/surface/tools/coverage-tools.js +27 -1
  220. package/dist/surface/tools/error-recovery.js +4 -4
  221. package/dist/surface/tools/feedback-invite.js +6 -4
  222. package/dist/surface/tools/harness-explore.d.ts +3 -2
  223. package/dist/surface/tools/harness-explore.js +7 -6
  224. package/dist/surface/tools/lease-tools.js +15 -6
  225. package/dist/surface/tools/live-call-text.js +4 -1
  226. package/dist/surface/tools/observe-tools.js +9 -1
  227. package/dist/surface/tools/tools.js +4 -0
  228. package/dist/telemetry/argument-shape.d.ts +0 -2
  229. package/dist/telemetry/argument-shape.js +0 -4
  230. package/dist/telemetry/error-class.d.ts +2 -3
  231. package/dist/telemetry/error-class.js +2 -3
  232. package/dist/telemetry/harness-drive.d.ts +0 -2
  233. package/dist/telemetry/harness-drive.js +0 -4
  234. package/dist/wire-journal.d.ts +3 -0
  235. package/dist/wire-journal.js +4 -0
  236. package/dist/workspace-packages.d.ts +0 -2
  237. package/dist/workspace-packages.js +1 -9
  238. package/docs/README.md +4 -2
  239. package/docs/actions.mdx +3 -3
  240. package/docs/agent-cheatsheet.md +4 -4
  241. package/docs/architecture.md +6 -6
  242. package/docs/best-practices.mdx +1 -1
  243. package/docs/capabilities.mdx +32 -1
  244. package/docs/cli/cloud.mdx +14 -4
  245. package/docs/cli/doctor.mdx +4 -0
  246. package/docs/cli/feedback.mdx +2 -2
  247. package/docs/cli/gate.mdx +9 -4
  248. package/docs/cli/update.mdx +10 -0
  249. package/docs/cli.mdx +9 -0
  250. package/docs/desktop.mdx +14 -10
  251. package/docs/environment.mdx +87 -0
  252. package/docs/faq.mdx +18 -16
  253. package/docs/first-drive.md +5 -13
  254. package/docs/flows.md +21 -2
  255. package/docs/frameworks.mdx +29 -10
  256. package/docs/getting-started.md +3 -3
  257. package/docs/http-transport.md +6 -3
  258. package/docs/human-control.md +2 -2
  259. package/docs/index.mdx +16 -6
  260. package/docs/install-agentic.mdx +1 -1
  261. package/docs/install-manual.mdx +1 -1
  262. package/docs/integration-patterns.md +3 -3
  263. package/docs/multi-agent-testing.md +1 -1
  264. package/docs/packages/babel-plugin.mdx +1 -1
  265. package/docs/packages/browser.mdx +2 -2
  266. package/docs/packages/react.mdx +2 -2
  267. package/docs/packages/tauri.mdx +1 -1
  268. package/docs/packages/vite-plugin.mdx +2 -2
  269. package/docs/packages.mdx +9 -9
  270. package/docs/predicates.mdx +50 -5
  271. package/docs/quickstart.mdx +2 -2
  272. package/docs/recipes.mdx +2 -2
  273. package/docs/state-management.mdx +2 -2
  274. package/docs/system-map.md +1 -1
  275. package/docs/telemetry.md +1 -1
  276. package/docs/token-efficiency.md +3 -3
  277. package/docs/tools/act-and-wait.mdx +1 -0
  278. package/docs/tools/act-sequence.mdx +1 -1
  279. package/docs/tools/console.mdx +1 -1
  280. package/docs/tools/inspect.mdx +1 -1
  281. package/docs/tools/network.mdx +1 -1
  282. package/docs/tools/query.mdx +1 -1
  283. package/docs/tools/sessions.mdx +1 -1
  284. package/docs/tools/snapshot.mdx +1 -1
  285. package/docs/tools/state.mdx +1 -1
  286. package/docs/tools/wait-for.mdx +1 -1
  287. package/docs/usage.md +7 -6
  288. package/docs/use-cases.mdx +157 -0
  289. package/docs/vs-screenshots.mdx +3 -3
  290. package/docs/what-is-recorded.md +2 -2
  291. package/docs/what-is-reticle.mdx +17 -20
  292. package/docs/why-reticle.mdx +14 -21
  293. package/package.json +10 -9
  294. package/dist/judgement/capsule/minimize.d.ts +0 -12
  295. package/dist/judgement/capsule/minimize.js +0 -22
  296. package/dist/judgement/phenomena/phenomena.d.ts +0 -27
  297. package/dist/judgement/phenomena/phenomena.js +0 -101
  298. package/dist/language/flows/flow-report.d.ts +0 -22
  299. package/dist/language/flows/flow-report.js +0 -106
@@ -0,0 +1,30 @@
1
+ export async function startJsCoverage(page, covering) {
2
+ if (covering.has(page))
3
+ return true;
4
+ try {
5
+ await page.coverage.startJSCoverage({ resetOnNavigation: false });
6
+ covering.add(page);
7
+ return true;
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }
13
+ export async function takeJsCoverage(page, covering) {
14
+ // Nothing collects until something asks: collection slows every script on the page. The first
15
+ // take starts it and has nothing to report yet.
16
+ if (!covering.has(page)) {
17
+ await startJsCoverage(page, covering);
18
+ return undefined;
19
+ }
20
+ try {
21
+ const entries = await page.coverage.stopJSCoverage();
22
+ covering.delete(page);
23
+ await startJsCoverage(page, covering);
24
+ return entries;
25
+ }
26
+ catch {
27
+ covering.delete(page);
28
+ return undefined;
29
+ }
30
+ }
@@ -9,6 +9,7 @@
9
9
  * Node-only. Playwright is loaded via DYNAMIC `import('playwright')` so non-CDP users never
10
10
  * pay for it; the type-only import is elided by `tsc`, so the build stays green without it.
11
11
  */
12
+ import { type ScriptCoverage } from './js-coverage.js';
12
13
  import type { Browser, Page } from 'playwright';
13
14
  import { ActionType, DriveErrorCode } from '@reticlehq/core';
14
15
  import { type MockRule } from './network-mock.js';
@@ -77,6 +78,14 @@ export interface RealInputProvider {
77
78
  * Optional: a provider with no owned browser simply omits it.
78
79
  */
79
80
  setMocks?(sessionUrl: string, rules: MockRule[]): Promise<boolean>;
81
+ /**
82
+ * What of the app's own code ran since the last take (V8 coverage, Chromium only), or undefined
83
+ * when no driven page matches or collection had not started. The first call starts collecting,
84
+ * because collection slows every script on the page and a slower page can let a console error land
85
+ * after a clean-console check passed. Optional: a provider with no owned browser cannot see the
86
+ * engine's counters.
87
+ */
88
+ takeCodeCoverage?(sessionUrl: string): Promise<ScriptCoverage[] | undefined>;
80
89
  /**
81
90
  * Pin the correlated page's viewport to fixed pixel dimensions so a screenshot baseline is
82
91
  * reproducible across machines (the missing piece of CI-stable visual regression, alongside masks
@@ -144,6 +153,7 @@ interface CdpProviderOptions {
144
153
  export declare class CdpRealInputProvider implements RealInputProvider {
145
154
  #private;
146
155
  constructor(options: CdpProviderOptions);
156
+ takeCodeCoverage(sessionUrl: string): Promise<ScriptCoverage[] | undefined>;
147
157
  isAvailableFor(sessionUrl: string): Promise<boolean>;
148
158
  perform(sessionUrl: string, action: ActionType, box: ElementBox, args: RealInputArgs): Promise<RealInputResult>;
149
159
  /** PNG of the correlated page, or undefined if none matches. */
@@ -236,6 +246,7 @@ export declare class LaunchedRealInputProvider implements OwnedRealInputProvider
236
246
  width: number;
237
247
  height: number;
238
248
  }): Promise<boolean>;
249
+ takeCodeCoverage(_sessionUrl: string): Promise<ScriptCoverage[] | undefined>;
239
250
  /** Apply network-mock rules to the owned page; false before navigate / after dispose. */
240
251
  setMocks(_sessionUrl: string, rules: MockRule[]): Promise<boolean>;
241
252
  /** Close the owned browser once. Idempotent and safe before navigate. */
@@ -1,6 +1,18 @@
1
+ /**
2
+ * Optional CDP/Playwright real-input mode.
3
+ *
4
+ * Synthetic `dispatchEvent` cannot drive native hover/pointer state (an onMouseEnter never
5
+ * fires, hit-testing never runs). When a CDP endpoint is configured, this module connects a
6
+ * Playwright `Browser` over CDP and drives REAL pointer/keyboard input against the element box
7
+ * the SDK resolves (viewport CSS px from getBoundingClientRect).
8
+ *
9
+ * Node-only. Playwright is loaded via DYNAMIC `import('playwright')` so non-CDP users never
10
+ * pay for it; the type-only import is elided by `tsc`, so the build stays green without it.
11
+ */
12
+ import { takeJsCoverage } from './js-coverage.js';
1
13
  import { stampedDriveUrl } from './drive-url-stamp.js';
2
- import { chromiumLaunchOptions } from '../../chromium-launch-options.js';
3
- import { gotoOptions } from '../pool/playwright-launcher.js';
14
+ import { launchChromium } from '../../launch-chromium.js';
15
+ import { chromiumLaunchHint, gotoOptions } from '../pool/playwright-launcher.js';
4
16
  import { BrowserLaunchKind } from '@reticlehq/core/telemetry';
5
17
  import { getSessionMetrics } from '../../telemetry/session-metrics.js';
6
18
  import { classifyConnectFailure } from '../../telemetry/connect-failure.js';
@@ -131,6 +143,8 @@ export class CdpRealInputProvider {
131
143
  /** Pages already listening. #pageFor resolves on EVERY call, so without this each action would add
132
144
  * another listener and every response would be emitted once per action taken so far. */
133
145
  #listening = new WeakSet();
146
+ /** Pages collecting V8 coverage — see js-coverage.ts. */
147
+ #covering = new WeakSet();
134
148
  #browser;
135
149
  constructor(options) {
136
150
  this.#cdpUrl = options.cdpUrl;
@@ -176,6 +190,10 @@ export class CdpRealInputProvider {
176
190
  this.#listen(page);
177
191
  return page;
178
192
  }
193
+ async takeCodeCoverage(sessionUrl) {
194
+ const page = await this.#pageFor(sessionUrl);
195
+ return page === undefined ? undefined : takeJsCoverage(page, this.#covering);
196
+ }
179
197
  async isAvailableFor(sessionUrl) {
180
198
  try {
181
199
  return (await this.#pageFor(sessionUrl)) !== undefined;
@@ -273,13 +291,14 @@ export const launchedChromium = async (headless) => {
273
291
  }
274
292
  const settle = getSessionMetrics().recordConnectAttempt(BrowserLaunchKind.LAUNCHED);
275
293
  try {
276
- const browser = await mod.chromium.launch(chromiumLaunchOptions(headless));
294
+ const browser = await launchChromium(mod.chromium, headless);
277
295
  settle();
278
296
  return browser;
279
297
  }
280
298
  catch (e) {
281
299
  settle(classifyConnectFailure(e));
282
- throw new DriveError(DriveErrorCode.LAUNCH_FAILED, e instanceof Error ? e.message : String(e));
300
+ const msg = e instanceof Error ? e.message : String(e);
301
+ throw new DriveError(DriveErrorCode.LAUNCH_FAILED, chromiumLaunchHint(msg) ?? msg);
283
302
  }
284
303
  };
285
304
  /**
@@ -296,6 +315,8 @@ export class LaunchedRealInputProvider {
296
315
  #onNetworkDetail;
297
316
  #browser;
298
317
  #page;
318
+ /** Pages collecting V8 coverage — see js-coverage.ts. */
319
+ #covering = new WeakSet();
299
320
  constructor(options) {
300
321
  this.#driveUrl = options.driveUrl;
301
322
  this.#headless = options.headless;
@@ -442,6 +463,10 @@ export class LaunchedRealInputProvider {
442
463
  await page.setViewportSize({ width: size.width, height: size.height });
443
464
  return true;
444
465
  }
466
+ takeCodeCoverage(_sessionUrl) {
467
+ const page = this.#livePage();
468
+ return page === undefined ? Promise.resolve(undefined) : takeJsCoverage(page, this.#covering);
469
+ }
445
470
  /** Apply network-mock rules to the owned page; false before navigate / after dispose. */
446
471
  async setMocks(_sessionUrl, rules) {
447
472
  const page = this.#livePage();
@@ -25,6 +25,12 @@ export declare function gotoOptions(timeoutMs: number | undefined): {
25
25
  export declare function playwrightLauncher(opts?: {
26
26
  headless?: boolean;
27
27
  }): Launcher;
28
+ /**
29
+ * Turn Playwright's raw launch error into the one command that fixes it, or undefined when it is
30
+ * not a missing-browser failure. Shared by the pooled and the drive launch, so `reticle verify` on
31
+ * a fresh machine gets the same advice as a lease.
32
+ */
33
+ export declare function chromiumLaunchHint(msg: string): string | undefined;
28
34
  /**
29
35
  * Resolve the pool's concurrency cap. An explicit RETICLE_MAX_CONTEXTS wins (clamped to
30
36
  * [1, hard ceiling]); otherwise scale with the machine but never above the CPU-derived ceiling.
@@ -6,7 +6,7 @@
6
6
  * Headless by default — the pool exists for fault-tolerant headless multi-agent testing.
7
7
  */
8
8
  import { BrowserLaunchKind } from '@reticlehq/core/telemetry';
9
- import { chromiumLaunchOptions } from '../../chromium-launch-options.js';
9
+ import { launchChromium } from '../../launch-chromium.js';
10
10
  import { getSessionMetrics } from '../../telemetry/session-metrics.js';
11
11
  import { classifyConnectFailure } from '../../telemetry/connect-failure.js';
12
12
  import { chromiumInstallCommand, chromiumInstallDepsCommand, bundledPlaywrightVersion, } from '../../command/cli/doctor/browser/chromium-hint.js';
@@ -105,27 +105,34 @@ export function playwrightLauncher(opts = {}) {
105
105
  // number that actually costs memory and the one that explains a slow machine.
106
106
  const settle = getSessionMetrics().recordConnectAttempt(BrowserLaunchKind.POOLED);
107
107
  try {
108
- const browser = wrapBrowser(await chromium.launch(chromiumLaunchOptions(headless)));
108
+ const browser = wrapBrowser(await launchChromium(chromium, headless));
109
109
  settle();
110
110
  return browser;
111
111
  }
112
112
  catch (err) {
113
113
  settle(classifyConnectFailure(err));
114
- // Turn Playwright's raw "Executable doesn't exist" into the one command that fixes it.
115
114
  const msg = err instanceof Error ? err.message : String(err);
116
- // Checked FIRST: Playwright's host-requirement validation error also contains
117
- // "browserType.launch", so the generic check below would otherwise misdiagnose it as a
118
- // missing binary and send the user through a fix that changes nothing.
119
- if (/host system is missing dependencies/i.test(msg)) {
120
- throw new Error(CHROMIUM_MISSING_DEPS_HINT);
121
- }
122
- if (/executable doesn.?t exist|playwright install|browsertype\.launch/i.test(msg)) {
123
- throw new Error(CHROMIUM_MISSING_HINT);
124
- }
125
- throw err;
115
+ const hint = chromiumLaunchHint(msg);
116
+ throw hint !== undefined ? new Error(hint) : err;
126
117
  }
127
118
  };
128
119
  }
120
+ /**
121
+ * Turn Playwright's raw launch error into the one command that fixes it, or undefined when it is
122
+ * not a missing-browser failure. Shared by the pooled and the drive launch, so `reticle verify` on
123
+ * a fresh machine gets the same advice as a lease.
124
+ */
125
+ export function chromiumLaunchHint(msg) {
126
+ // Checked FIRST: Playwright's host-requirement validation error also contains
127
+ // "browserType.launch", so the generic check below would otherwise misdiagnose it as a
128
+ // missing binary and send the user through a fix that changes nothing.
129
+ if (/host system is missing dependencies/i.test(msg))
130
+ return CHROMIUM_MISSING_DEPS_HINT;
131
+ if (/executable doesn.?t exist|playwright install|browsertype\.launch/i.test(msg)) {
132
+ return CHROMIUM_MISSING_HINT;
133
+ }
134
+ return undefined;
135
+ }
129
136
  const MAX_CONTEXTS_CEILING = 8;
130
137
  const MAX_CONTEXTS_FLOOR = 1;
131
138
  /**
@@ -12,8 +12,8 @@
12
12
  export interface DevCommand {
13
13
  /** The literal shell command, e.g. `pnpm run dev`. */
14
14
  command: string;
15
- /** The package.json script it runs. */
16
- script: string;
15
+ /** The package.json script it runs. Absent when the command runs a CLI directly (`npx tauri dev`). */
16
+ script?: string;
17
17
  /** The port the script PINS, when it pins one. Absent means the tool picks — never a guess. */
18
18
  port?: number;
19
19
  }
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { readFileSync } from 'node:fs';
12
12
  import { join } from 'node:path';
13
+ import { desktopLaunch } from '@reticlehq/init';
13
14
  /**
14
15
  * Script names checked, in order.
15
16
  *
@@ -47,23 +48,26 @@ function pinnedPort(script) {
47
48
  const port = Number.parseInt(digits, 10);
48
49
  return Number.isSafeInteger(port) ? port : undefined;
49
50
  }
50
- /** The scripts block of a package.json, or an empty one for anything unreadable/unparseable. */
51
- function readScripts(raw) {
51
+ /** The parsed package.json, or undefined for anything unreadable/unparseable. */
52
+ function readManifest(raw) {
52
53
  if (raw === undefined)
53
- return {};
54
+ return undefined;
54
55
  try {
55
- const parsed = JSON.parse(raw);
56
- if (null === parsed || 'object' !== typeof parsed)
57
- return {};
58
- const scripts = parsed.scripts;
59
- if (null === scripts || 'object' !== typeof scripts)
60
- return {};
61
- return scripts;
56
+ return JSON.parse(raw);
62
57
  }
63
58
  catch {
64
- return {};
59
+ return undefined;
65
60
  }
66
61
  }
62
+ /** The scripts block of a parsed package.json, or an empty one. */
63
+ function scriptsOf(manifest) {
64
+ if (null === manifest || 'object' !== typeof manifest)
65
+ return {};
66
+ const scripts = manifest.scripts;
67
+ if (null === scripts || 'object' !== typeof scripts)
68
+ return {};
69
+ return scripts;
70
+ }
67
71
  /**
68
72
  * The dev command for the project in `directory`, or undefined when there is not one to be sure of.
69
73
  *
@@ -71,9 +75,23 @@ function readScripts(raw) {
71
75
  * default disk reader.
72
76
  */
73
77
  export function detectDevCommand(directory, read = readTextFile) {
74
- const scripts = readScripts(read(join(directory, PACKAGE_JSON)));
78
+ const manifest = readManifest(read(join(directory, PACKAGE_JSON)));
79
+ const scripts = scriptsOf(manifest);
75
80
  const manager = LOCKFILES.find(({ file }) => read(join(directory, file)) !== undefined)?.manager ??
76
81
  DEFAULT_PACKAGE_MANAGER;
82
+ // A desktop shell's `dev` is its renderer's dev server, which no window ever opens onto. init's
83
+ // rule decides which launcher it is, so the command the daemon hands an agent and the one init
84
+ // spawns are the same.
85
+ const desktop = desktopLaunch(manifest);
86
+ if (desktop !== undefined) {
87
+ if ('command' in desktop)
88
+ return { command: desktop.command };
89
+ const command = `${manager} run ${desktop.script}`;
90
+ return {
91
+ command: 0 < desktop.args.length ? `${command} ${desktop.args}` : command,
92
+ script: desktop.script,
93
+ };
94
+ }
77
95
  for (const script of DEV_SCRIPTS) {
78
96
  const body = scripts[script];
79
97
  if ('string' !== typeof body || 0 === body.trim().length)
@@ -64,6 +64,15 @@ export function classifyPort(port, probe) {
64
64
  * accepts v4-mapped connections — so between the two, every ordinary bind is covered.
65
65
  */
66
66
  export const PROBE_HOSTS = ['127.0.0.1', '::1'];
67
+ /**
68
+ * The name the probe gives in its Host header, whichever address it dialled.
69
+ *
70
+ * Node names the dialled address, so the `::1` probe sent `Host: [::1]:4200`. Angular's SSR dev
71
+ * server validates Host, answered 400, and printed `ERROR: Bad Request ("http://[::1]:4200/")` into
72
+ * the user's dev terminal on every sweep, for as long as the daemon ran. The address picks the
73
+ * family; the header says what a browser on this machine would.
74
+ */
75
+ const PROBE_HOST_HEADER = 'localhost';
67
76
  /**
68
77
  * Does this port serve a DOCUMENT — i.e. is it a dev server rather than merely something listening?
69
78
  *
@@ -76,7 +85,14 @@ function probeState(port, host) {
76
85
  // Whether the SOCKET came up is tracked separately from whether a response did, because that is
77
86
  // the whole difference between "nothing is there" and "something is there and still compiling".
78
87
  let connected = false;
79
- const req = request({ host, port, path: '/', method: 'GET', timeout: HTTP_PROBE_TIMEOUT_MS }, (res) => {
88
+ const req = request({
89
+ host,
90
+ port,
91
+ path: '/',
92
+ method: 'GET',
93
+ timeout: HTTP_PROBE_TIMEOUT_MS,
94
+ headers: { host: `${PROBE_HOST_HEADER}:${String(port)}` },
95
+ }, (res) => {
80
96
  const answer = looksLikeDevServer(res.statusCode ?? 0, res.headers['content-type']);
81
97
  res.resume(); // drain, so the socket closes rather than lingering
82
98
  resolve(answer ? PortState.SERVES_DOCUMENT : PortState.CLOSED);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * What does the route a tab died on answer right now?
3
+ *
4
+ * A route that throws server-side tears the page down and the SDK never reconnects, so the daemon
5
+ * is left with an empty session list and the URL it last saw (#862). The URL alone cannot separate
6
+ * "the route 500s" from "somebody closed the tab" — one plain GET can, and the daemon is the only
7
+ * party placed to make it: the page is gone, the agent is inside an MCP call, and the daemon is on
8
+ * the same machine as the dev server.
9
+ *
10
+ * What it refuses to do, because this runs unattended in a diagnostic loop:
11
+ * - only loopback hostnames — the daemon is localhost-only and a diagnostic must not reach out;
12
+ * - only `http:` — `https:` on localhost means a self-signed certificate, and fetching it with
13
+ * verification off would be a posture change for a status code;
14
+ * - GET only, redirects not followed, the body never read — the status is the only thing that
15
+ * leaves the socket, and it is drained so the connection closes rather than lingers;
16
+ * - an error or a timeout is `undefined`, never a status. No fact is better than a wrong one.
17
+ *
18
+ * Same shape as its neighbour `dev-server-probe.ts`, which asks a port whether it serves a document; this asks
19
+ * one exact path what it answers.
20
+ */
21
+ /** Short on purpose: a dev server that has not answered a route in this long is not going to. */
22
+ export declare const ROUTE_STATUS_TIMEOUT_MS = 2000;
23
+ /** The HTTP status `url` answers a GET with, or undefined when there is no answer to report. */
24
+ export declare function probeRouteStatus(url: string, timeoutMs?: number): Promise<number | undefined>;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * What does the route a tab died on answer right now?
3
+ *
4
+ * A route that throws server-side tears the page down and the SDK never reconnects, so the daemon
5
+ * is left with an empty session list and the URL it last saw (#862). The URL alone cannot separate
6
+ * "the route 500s" from "somebody closed the tab" — one plain GET can, and the daemon is the only
7
+ * party placed to make it: the page is gone, the agent is inside an MCP call, and the daemon is on
8
+ * the same machine as the dev server.
9
+ *
10
+ * What it refuses to do, because this runs unattended in a diagnostic loop:
11
+ * - only loopback hostnames — the daemon is localhost-only and a diagnostic must not reach out;
12
+ * - only `http:` — `https:` on localhost means a self-signed certificate, and fetching it with
13
+ * verification off would be a posture change for a status code;
14
+ * - GET only, redirects not followed, the body never read — the status is the only thing that
15
+ * leaves the socket, and it is drained so the connection closes rather than lingers;
16
+ * - an error or a timeout is `undefined`, never a status. No fact is better than a wrong one.
17
+ *
18
+ * Same shape as its neighbour `dev-server-probe.ts`, which asks a port whether it serves a document; this asks
19
+ * one exact path what it answers.
20
+ */
21
+ import { request } from 'node:http';
22
+ import { isLoopbackHostname } from '@reticlehq/core';
23
+ /** Short on purpose: a dev server that has not answered a route in this long is not going to. */
24
+ export const ROUTE_STATUS_TIMEOUT_MS = 2_000;
25
+ const HTTP = 'http:';
26
+ /** The HTTP status `url` answers a GET with, or undefined when there is no answer to report. */
27
+ export function probeRouteStatus(url, timeoutMs = ROUTE_STATUS_TIMEOUT_MS) {
28
+ let parsed;
29
+ try {
30
+ parsed = new URL(url);
31
+ }
32
+ catch {
33
+ return Promise.resolve(undefined);
34
+ }
35
+ if (parsed.protocol !== HTTP || !isLoopbackHostname(parsed.hostname)) {
36
+ return Promise.resolve(undefined);
37
+ }
38
+ return new Promise((resolve) => {
39
+ const req = request({
40
+ host: parsed.hostname,
41
+ port: parsed.port,
42
+ path: `${parsed.pathname}${parsed.search}`,
43
+ method: 'GET',
44
+ timeout: timeoutMs,
45
+ }, (res) => {
46
+ const status = res.statusCode;
47
+ res.resume(); // drain, so the socket closes rather than lingering
48
+ resolve(status);
49
+ });
50
+ req.once('timeout', () => {
51
+ req.destroy();
52
+ resolve(undefined);
53
+ });
54
+ req.once('error', () => resolve(undefined));
55
+ req.end();
56
+ });
57
+ }
@@ -48,6 +48,8 @@ export declare class ObservedState {
48
48
  actedRefs(): ReadonlySet<string>;
49
49
  /** Every driven control's label, for matching across re-renders. */
50
50
  actedLabels(): ReadonlySet<string>;
51
+ recordProvedLabel(label: string): void;
52
+ provedLabels(): ReadonlySet<string>;
51
53
  /**
52
54
  * Record bridge-side sampling as a blind spot.
53
55
  *
@@ -58,3 +60,12 @@ export declare class ObservedState {
58
60
  noteRateLimited(dropped: number): void;
59
61
  blindSpots(): Readonly<Record<string, number>>;
60
62
  }
63
+ /**
64
+ * The snapshot-shaped labels an act reply names its control by: the testid, and `role "name"`.
65
+ *
66
+ * The TESTID first: it is the strongest identity a control has and it survives any re-render.
67
+ * Coverage previously matched only on `role "name"`, so a control with a testid but no accessible
68
+ * name — or on a stack where the act reply carried neither — was unrecognisable after a re-render,
69
+ * and coverage read `exercised: 0` however much work had been done.
70
+ */
71
+ export declare function controlLabelsOf(payload: unknown): string[];
@@ -105,6 +105,18 @@ export class ObservedState {
105
105
  actedLabels() {
106
106
  return this.#actedLabels;
107
107
  }
108
+ /**
109
+ * Controls whose action came back `verified: "yes"` on a declared consequence — the PROVED level
110
+ * of the coverage ledger. Same label identity as `actedLabels`, so the two compare directly.
111
+ */
112
+ #provedLabels = new Set();
113
+ recordProvedLabel(label) {
114
+ if (label.length > 0)
115
+ this.#provedLabels.add(label);
116
+ }
117
+ provedLabels() {
118
+ return this.#provedLabels;
119
+ }
108
120
  /**
109
121
  * Record bridge-side sampling as a blind spot.
110
122
  *
@@ -119,3 +131,25 @@ export class ObservedState {
119
131
  return this.#blindSpots;
120
132
  }
121
133
  }
134
+ /**
135
+ * The snapshot-shaped labels an act reply names its control by: the testid, and `role "name"`.
136
+ *
137
+ * The TESTID first: it is the strongest identity a control has and it survives any re-render.
138
+ * Coverage previously matched only on `role "name"`, so a control with a testid but no accessible
139
+ * name — or on a stack where the act reply carried neither — was unrecognisable after a re-render,
140
+ * and coverage read `exercised: 0` however much work had been done.
141
+ */
142
+ export function controlLabelsOf(payload) {
143
+ if (typeof payload !== 'object' || null === payload)
144
+ return [];
145
+ const record = payload;
146
+ const labels = [];
147
+ const testid = record['testid'];
148
+ if ('string' === typeof testid && testid.length > 0)
149
+ labels.push(testid);
150
+ const role = record['role'];
151
+ const name = record['name'];
152
+ if ('string' === typeof role && 'string' === typeof name && role.length > 0 && name.length > 0)
153
+ labels.push(`${role} "${name}"`);
154
+ return labels;
155
+ }
@@ -91,6 +91,20 @@ export interface NoSessionFacts {
91
91
  * already read it; the message did not.
92
92
  */
93
93
  authRefused?: boolean;
94
+ /**
95
+ * The bridge refused the last hello for something OTHER than its token — a protocol number out of
96
+ * range, or a hello that failed the wire schema outright — and nothing has connected since.
97
+ *
98
+ * The same positive evidence as `authRefused`, with a different fix. Without it the daemon had
99
+ * recorded the refusal and `init` still ended on "the SDK is in the page and never dialled the
100
+ * bridge", about a page that dialled and was turned away. `reason` follows the `cause — fix`
101
+ * shape the bridge writes; the page and project are what the refused hello said about itself.
102
+ */
103
+ helloRefused?: {
104
+ reason: string;
105
+ url?: string;
106
+ projectId?: string;
107
+ };
94
108
  /**
95
109
  * The framework `.reticle.json` declares, when there is one.
96
110
  *
@@ -100,6 +114,14 @@ export interface NoSessionFacts {
100
114
  * hint the agent reads hours later never mentioned it.
101
115
  */
102
116
  framework?: string;
117
+ /**
118
+ * The project is a desktop shell (Electron, Tauri), read from the project the way `doctor` reads it.
119
+ *
120
+ * Its page exists only inside its own window, so every "open the app's URL in a browser" and
121
+ * every lease offer below is the wrong move there: it loads the renderer without the shell's IPC
122
+ * bridge or native APIs, which is not the app being verified. Absent means web.
123
+ */
124
+ desktop?: boolean;
103
125
  /**
104
126
  * `.reticle.json` files found OUTSIDE this daemon's directory — a workspace app, a repo root.
105
127
  *
@@ -144,6 +166,13 @@ export interface NoSessionFacts {
144
166
  * or the ring has forgotten — inventing a URL is worse than the silence this exists to end.
145
167
  */
146
168
  lastKnownUrl?: string;
169
+ /**
170
+ * What a plain GET of `lastKnownUrl` answered, done out of band by the watch after the session
171
+ * went. The one fact that separates a server error from a closed tab and from an install
172
+ * problem (#808, option 3). Present only when the fetch got an HTTP status back — a failed or
173
+ * timed-out fetch is no fact, and this stays absent rather than guessing.
174
+ */
175
+ lastKnownStatus?: number;
147
176
  }
148
177
  /**
149
178
  * Every branch ends with this, and `recoveryFor` keys on it to suppress the generic no-session