@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
@@ -21,7 +21,6 @@
21
21
  */
22
22
  import { z } from 'zod';
23
23
  import { type Intent } from '@reticlehq/core/artifacts';
24
- import type { FileSystemPort } from '../project/fs/fs-port.js';
25
24
  /**
26
25
  * How settled a record is.
27
26
  *
@@ -52,12 +51,12 @@ declare const IntentRecordSchema: z.ZodObject<{
52
51
  flow: z.ZodOptional<z.ZodString>;
53
52
  files: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
54
53
  }, "strip", z.ZodTypeAny, {
55
- flow?: string | undefined;
56
54
  route?: string | undefined;
55
+ flow?: string | undefined;
57
56
  files?: string[] | undefined;
58
57
  }, {
59
- flow?: string | undefined;
60
58
  route?: string | undefined;
59
+ flow?: string | undefined;
61
60
  files?: string[] | undefined;
62
61
  }>>;
63
62
  provenBy: z.ZodOptional<z.ZodObject<{
@@ -101,8 +100,8 @@ declare const IntentRecordSchema: z.ZodObject<{
101
100
  statement: string;
102
101
  declaredAt: number;
103
102
  surface?: {
104
- flow?: string | undefined;
105
103
  route?: string | undefined;
104
+ flow?: string | undefined;
106
105
  files?: string[] | undefined;
107
106
  } | undefined;
108
107
  source?: string | undefined;
@@ -126,8 +125,8 @@ declare const IntentRecordSchema: z.ZodObject<{
126
125
  statement: string;
127
126
  declaredAt: number;
128
127
  surface?: {
129
- flow?: string | undefined;
130
128
  route?: string | undefined;
129
+ flow?: string | undefined;
131
130
  files?: string[] | undefined;
132
131
  } | undefined;
133
132
  source?: string | undefined;
@@ -159,12 +158,12 @@ export declare const IntentShardSchema: z.ZodObject<{
159
158
  flow: z.ZodOptional<z.ZodString>;
160
159
  files: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
161
160
  }, "strip", z.ZodTypeAny, {
162
- flow?: string | undefined;
163
161
  route?: string | undefined;
162
+ flow?: string | undefined;
164
163
  files?: string[] | undefined;
165
164
  }, {
166
- flow?: string | undefined;
167
165
  route?: string | undefined;
166
+ flow?: string | undefined;
168
167
  files?: string[] | undefined;
169
168
  }>>;
170
169
  provenBy: z.ZodOptional<z.ZodObject<{
@@ -208,8 +207,8 @@ export declare const IntentShardSchema: z.ZodObject<{
208
207
  statement: string;
209
208
  declaredAt: number;
210
209
  surface?: {
211
- flow?: string | undefined;
212
210
  route?: string | undefined;
211
+ flow?: string | undefined;
213
212
  files?: string[] | undefined;
214
213
  } | undefined;
215
214
  source?: string | undefined;
@@ -233,8 +232,8 @@ export declare const IntentShardSchema: z.ZodObject<{
233
232
  statement: string;
234
233
  declaredAt: number;
235
234
  surface?: {
236
- flow?: string | undefined;
237
235
  route?: string | undefined;
236
+ flow?: string | undefined;
238
237
  files?: string[] | undefined;
239
238
  } | undefined;
240
239
  source?: string | undefined;
@@ -262,8 +261,8 @@ export declare const IntentShardSchema: z.ZodObject<{
262
261
  statement: string;
263
262
  declaredAt: number;
264
263
  surface?: {
265
- flow?: string | undefined;
266
264
  route?: string | undefined;
265
+ flow?: string | undefined;
267
266
  files?: string[] | undefined;
268
267
  } | undefined;
269
268
  source?: string | undefined;
@@ -291,8 +290,8 @@ export declare const IntentShardSchema: z.ZodObject<{
291
290
  statement: string;
292
291
  declaredAt: number;
293
292
  surface?: {
294
- flow?: string | undefined;
295
293
  route?: string | undefined;
294
+ flow?: string | undefined;
296
295
  files?: string[] | undefined;
297
296
  } | undefined;
298
297
  source?: string | undefined;
@@ -311,24 +310,6 @@ export declare const IntentShardSchema: z.ZodObject<{
311
310
  }>;
312
311
  }>;
313
312
  export type IntentShard = z.infer<typeof IntentShardSchema>;
314
- /** One line per intent: enough to decide whether to open the shard, and no more. */
315
- declare const IntentIndexEntrySchema: z.ZodObject<{
316
- id: z.ZodString;
317
- subject: z.ZodString;
318
- statement: z.ZodString;
319
- status: z.ZodEnum<["proposed", "agreed", "proved", "stale"]>;
320
- }, "strip", z.ZodTypeAny, {
321
- status: "proved" | "proposed" | "agreed" | "stale";
322
- id: string;
323
- subject: string;
324
- statement: string;
325
- }, {
326
- status: "proved" | "proposed" | "agreed" | "stale";
327
- id: string;
328
- subject: string;
329
- statement: string;
330
- }>;
331
- export type IntentIndexEntry = z.infer<typeof IntentIndexEntrySchema>;
332
313
  export declare const IntentIndexSchema: z.ZodObject<{
333
314
  version: z.ZodLiteral<1>;
334
315
  entries: z.ZodArray<z.ZodObject<{
@@ -366,7 +347,6 @@ export declare const IntentIndexSchema: z.ZodObject<{
366
347
  }>;
367
348
  export type IntentIndex = z.infer<typeof IntentIndexSchema>;
368
349
  export declare const emptyShard: (subject: string) => IntentShard;
369
- export declare const emptyIndex: () => IntentIndex;
370
350
  /** The verification lifecycle a legacy record carried, mapped onto the settledness axis. */
371
351
  export declare const statusFromState: (state: Intent["state"]) => IntentStatus;
372
352
  /**
@@ -383,6 +363,4 @@ export declare const indexFrom: (shards: readonly IntentShard[]) => IntentIndex;
383
363
  export declare const shardsFrom: (records: readonly IntentRecord[]) => IntentShard[];
384
364
  /** Serialise byte-stably, with keys sorted so a re-save of unchanged data is a no-op diff. */
385
365
  export declare const serialise: (value: unknown) => string;
386
- /** Read + validate one JSON file, failing soft: a hand-merged file with a conflict marker is real. */
387
- export declare const readJsonFile: <T>(fs: FileSystemPort, path: string, parse: (raw: unknown) => T, fallback: T) => Promise<T>;
388
366
  export {};
@@ -96,7 +96,6 @@ export const emptyShard = (subject) => ({
96
96
  subject,
97
97
  intents: {},
98
98
  });
99
- export const emptyIndex = () => ({ version: INTENT_SHARD_VERSION, entries: [] });
100
99
  /** The verification lifecycle a legacy record carried, mapped onto the settledness axis. */
101
100
  export const statusFromState = (state) => 'proved' === state ? IntentStatus.PROVED : IntentStatus.AGREED;
102
101
  /**
@@ -142,12 +141,3 @@ const sortKeys = (value) => {
142
141
  return value;
143
142
  return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)));
144
143
  };
145
- /** Read + validate one JSON file, failing soft: a hand-merged file with a conflict marker is real. */
146
- export const readJsonFile = async (fs, path, parse, fallback) => {
147
- try {
148
- return parse(JSON.parse(await fs.readFile(path)));
149
- }
150
- catch {
151
- return fallback;
152
- }
153
- };
@@ -1,4 +1,4 @@
1
- import type { FlowName, Predicate } from '@reticlehq/core';
1
+ import type { FlowFile, FlowName, Predicate } from '@reticlehq/core';
2
2
  /**
3
3
  * What this module needs a recorded step to BE, named structurally rather than imported.
4
4
  *
@@ -15,6 +15,10 @@ export interface TapeStep {
15
15
  expect?: Predicate;
16
16
  /** Recorder-internal, and the field journeys are cut on. Never written to the saved flow. */
17
17
  route?: string;
18
+ page?: string;
19
+ endPage?: string;
20
+ /** The intent the agent declared on this action. The first one names the journey. */
21
+ intent?: string;
18
22
  }
19
23
  /** The compiled shape a flow store accepts. Structural, for the same reason as `TapeStep`. */
20
24
  export interface DriveProgram {
@@ -22,6 +26,7 @@ export interface DriveProgram {
22
26
  version: number;
23
27
  steps: TapeStep[];
24
28
  startPath?: string;
29
+ author?: FlowFile['author'];
25
30
  }
26
31
  /**
27
32
  * What a session drove, saved as a replayable flow — without anybody asking for it.
@@ -36,21 +41,21 @@ export interface DriveProgram {
36
41
  */
37
42
  /** The one condition on saving, and the reason the whole thing is safe. */
38
43
  export declare function carriesAnAssertion(steps: readonly TapeStep[]): boolean;
44
+ /** Every auto-saved flow's name starts here, which is what lets a merge tell them from hand-named ones. */
45
+ export declare const DRIVE_FLOW_PREFIX = "drive-";
39
46
  /**
40
- * A stable name for what this session drove — led by what it proved.
47
+ * A readable name for a journey, led by WHY it was driven.
41
48
  *
42
- * Two properties, and the name used to have only the second. It has to be STABLE: teardown fires on
43
- * every socket close and a reconnecting tab keeps its id, so a name that moves leaves one journey
44
- * scattered across several files, each a partial copy of the others. And it has to be READABLE,
45
- * because `presenter-controls.ts` renders it straight into the replay button's `textContent` — so
46
- * the label a person actually sees was `drive-sdc991872-6d66-4adf-8780-f931c62905f9`, twice, for two
47
- * different journeys. A session id is a fact about the socket, not about the feature.
49
+ * The agent's own `intent` on the action is the best name there is — it is the sentence a person
50
+ * would use — so it leads when one was declared. Without one, what the journey proved names it (the
51
+ * strongest claim first), and the route follows so two journeys stay apart.
48
52
  *
49
- * So the claim leads, the route follows, and a SHORT session discriminator trails: two sessions that
50
- * prove the same thing on the same route stay separate files rather than overwriting each other,
51
- * which is the one thing the full id was buying that the claim cannot.
53
+ * No session id. It used to trail every name so two sessions never overwrote each other, which also
54
+ * made every session's drive of the same journey its own file. Two DIFFERENT journeys that land on
55
+ * one name are now told apart at save by their shape, and two copies of the SAME journey merge —
56
+ * see FlowStore.saveJourney.
52
57
  */
53
- export declare function driveFlowName(sessionId: string, route?: string, steps?: readonly TapeStep[]): FlowName;
58
+ export declare function driveFlowName(route?: string, steps?: readonly TapeStep[]): FlowName;
54
59
  export interface DriveFlowOutcome {
55
60
  /** The flows that were written — one per journey the session contained. */
56
61
  readonly saved?: readonly string[];
@@ -65,7 +70,7 @@ export interface DriveFlowOutcome {
65
70
  readonly unprovenSteps?: number;
66
71
  }
67
72
  /** A flow per journey, so two journeys in one session are two regression tests rather than one. */
68
- export declare function driveFlowsFrom(sessionId: string, tape: {
73
+ export declare function driveFlowsFrom(tape: {
69
74
  steps: readonly TapeStep[];
70
75
  startPath?: string;
71
76
  } | undefined): {
@@ -1,4 +1,4 @@
1
- import { PredicateKind, REPLAY_PROGRAM_VERSION, asFlowName } from '@reticlehq/core';
1
+ import { ActionType, PredicateKind, QueryBy, REPLAY_PROGRAM_VERSION, asFlowName, defaultIsSensitiveKey, } from '@reticlehq/core';
2
2
  import { clauseOfKind } from '@reticlehq/core';
3
3
  /**
4
4
  * What a session drove, saved as a replayable flow — without anybody asking for it.
@@ -30,7 +30,7 @@ const segment = (value, max) => value
30
30
  * DOM-shaped claims are the weakest. Naming a flow after the strongest thing it proves is the same
31
31
  * judgement the honesty grade makes, reused where a human reads it.
32
32
  *
33
- * Returns '' when a step claims nothing nameable, and the caller keeps the old session-only name.
33
+ * Returns '' when a step claims nothing nameable.
34
34
  */
35
35
  function provedSlug(steps) {
36
36
  for (const step of steps) {
@@ -63,42 +63,29 @@ function provedSlug(steps) {
63
63
  }
64
64
  return '';
65
65
  }
66
+ /** Every auto-saved flow's name starts here, which is what lets a merge tell them from hand-named ones. */
67
+ export const DRIVE_FLOW_PREFIX = 'drive-';
66
68
  /**
67
- * A stable name for what this session drove — led by what it proved.
69
+ * A readable name for a journey, led by WHY it was driven.
68
70
  *
69
- * Two properties, and the name used to have only the second. It has to be STABLE: teardown fires on
70
- * every socket close and a reconnecting tab keeps its id, so a name that moves leaves one journey
71
- * scattered across several files, each a partial copy of the others. And it has to be READABLE,
72
- * because `presenter-controls.ts` renders it straight into the replay button's `textContent` — so
73
- * the label a person actually sees was `drive-sdc991872-6d66-4adf-8780-f931c62905f9`, twice, for two
74
- * different journeys. A session id is a fact about the socket, not about the feature.
71
+ * The agent's own `intent` on the action is the best name there is — it is the sentence a person
72
+ * would use — so it leads when one was declared. Without one, what the journey proved names it (the
73
+ * strongest claim first), and the route follows so two journeys stay apart.
75
74
  *
76
- * So the claim leads, the route follows, and a SHORT session discriminator trails: two sessions that
77
- * prove the same thing on the same route stay separate files rather than overwriting each other,
78
- * which is the one thing the full id was buying that the claim cannot.
75
+ * No session id. It used to trail every name so two sessions never overwrote each other, which also
76
+ * made every session's drive of the same journey its own file. Two DIFFERENT journeys that land on
77
+ * one name are now told apart at save by their shape, and two copies of the SAME journey merge —
78
+ * see FlowStore.saveJourney.
79
79
  */
80
- export function driveFlowName(sessionId, route, steps = []) {
81
- const safe = sessionId.replace(/[^a-zA-Z0-9-]/g, '-').slice(0, 40);
82
- const proved = provedSlug(steps);
83
- // The route is part of the name, so two journeys in one session do not overwrite each other —
84
- // and so a re-drive of the same journey rewrites its own flow rather than adding a near-duplicate.
85
- const leg = (route ?? '')
86
- .replace(/[^a-zA-Z0-9]+/g, '-')
87
- .replace(/^-|-$/g, '')
88
- .slice(0, 24);
89
- // Minted here, not at the call site. This function is where a drive's flow name comes into
90
- // existence, and every character it can emit has just been forced through the safe-segment
91
- // replacements above — so this IS the validated boundary the brand is meant to be created at.
92
- // Returning `string` pushed a cast onto each caller, which is the hole `flowPath`'s parameter
93
- // type was added to close.
94
- // Nothing nameable was proved, so keep the name this function has always produced.
95
- if ('' === proved) {
96
- return asFlowName(0 === leg.length ? `drive-${safe}` : `drive-${safe}-${leg}`);
97
- }
98
- // Enough of the session to keep two of them apart, and little enough to read past.
99
- const discriminator = safe.replace(/-/g, '').slice(0, 8);
100
- const parts = ['drive', proved, ...(0 === leg.length ? [] : [leg]), discriminator];
101
- return asFlowName(parts.join('-'));
80
+ export function driveFlowName(route, steps = []) {
81
+ const intent = steps.find((step) => step.intent !== undefined)?.intent;
82
+ const leg = segment(route ?? '', 24);
83
+ const why = intent === undefined ? '' : segment(intent, 48);
84
+ const lead = '' !== why ? why : provedSlug(steps);
85
+ // Minted here: every character has just been forced through `segment`, so this IS the validated
86
+ // boundary the brand is meant to be created at.
87
+ const parts = [lead, ...('' !== why || 0 === leg.length ? [] : [leg])].filter((p) => '' !== p);
88
+ return asFlowName(`${DRIVE_FLOW_PREFIX}${0 === parts.length ? 'journey' : parts.join('-')}`);
102
89
  }
103
90
  /**
104
91
  * Turn this session's ambient tape into a flow, when it is worth one.
@@ -125,18 +112,39 @@ export function driveFlowName(sessionId, route, steps = []) {
125
112
  */
126
113
  function segmentsByRoute(steps) {
127
114
  const out = [];
115
+ let signedIn = false;
128
116
  for (const step of steps) {
129
117
  const last = out.at(-1);
130
- if (last !== undefined && last.route === step.route) {
118
+ const previous = last?.steps.at(-1);
119
+ // The same journey when the route is unchanged, or when the previous ACTION moved the app here
120
+ // — cart → shipping → payment is one flow, and cutting it left three fragments that each
121
+ // proved a page and none proved the journey. Not after a sign-in: that is the cut this exists
122
+ // for, and a login that navigates onward must still not open the next flow.
123
+ const caused = previous !== undefined && step.route !== undefined && previous.endPage === step.route;
124
+ if (last !== undefined && (last.route === step.route || (caused && !signedIn))) {
131
125
  last.steps.push(step);
126
+ signedIn ||= typesASecret(step);
132
127
  continue;
133
128
  }
129
+ signedIn = typesASecret(step);
134
130
  out.push({ ...(step.route === undefined ? {} : { route: step.route }), steps: [step] });
135
131
  }
136
132
  return out;
137
133
  }
134
+ /**
135
+ * A fill into a field the network channel would redact — the sign-in a journey must not carry.
136
+ * Read off the recorder's anchor args: a testid names the field by `value`, a role by `name`.
137
+ */
138
+ function typesASecret(step) {
139
+ const action = step.args['action'];
140
+ if (ActionType.FILL !== action && ActionType.TYPE !== action)
141
+ return false;
142
+ const by = step.args['by'];
143
+ const field = QueryBy.TESTID === by ? step.args['value'] : step.args['name'];
144
+ return 'string' === typeof field && defaultIsSensitiveKey(field);
145
+ }
138
146
  /** A flow per journey, so two journeys in one session are two regression tests rather than one. */
139
- export function driveFlowsFrom(sessionId, tape) {
147
+ export function driveFlowsFrom(tape) {
140
148
  if (tape === undefined || 0 === tape.steps.length)
141
149
  return { programs: [], outcome: {} };
142
150
  const programs = [];
@@ -148,7 +156,7 @@ export function driveFlowsFrom(sessionId, tape) {
148
156
  }
149
157
  const startPath = segment.route ?? tape.startPath;
150
158
  programs.push({
151
- name: driveFlowName(sessionId, startPath, segment.steps),
159
+ name: driveFlowName(startPath, segment.steps),
152
160
  version: REPLAY_PROGRAM_VERSION,
153
161
  // The route is recorder-internal and has no business on disk — `startPath` is where the
154
162
  // on-disk flow says the same thing, in the field replay actually reads.
@@ -49,6 +49,8 @@ export const WORKSPACE_TIERS = {
49
49
  [ReticleDir.AMBIENT_FILE]: localFile,
50
50
  [ReticleDir.ENVELOPES_FILE]: localFile,
51
51
  [ReticleDir.FLAKE_FILE]: localFile,
52
+ // What THIS machine's drives covered, and the best each level reached — the gate's ratchet.
53
+ [ReticleDir.COVERAGE_FILE]: localFile,
52
54
  [ReticleDir.TIERS_FILE]: localFile,
53
55
  // The user's own record of what Reticle did for them, on THIS machine.
54
56
  [ReticleDir.IMPACT_FILE]: localFile,
@@ -79,8 +79,16 @@ interface SessionEndDeps {
79
79
  * this handler follows.
80
80
  */
81
81
  flows?: {
82
- save: (program: DriveProgram, annotations?: undefined, projectId?: ProjectId) => Promise<unknown>;
82
+ saveJourney: (program: DriveProgram, projectId: ProjectId | undefined, prefix: string) => Promise<unknown>;
83
83
  };
84
+ /**
85
+ * The flow store for a session's own `.reticle`. Drive flows used to go to `flows` above — the
86
+ * daemon's launch directory — while replay reads the session's project, so a daemon started
87
+ * anywhere else saved flows nothing could replay. Absent, `flows` is used.
88
+ */
89
+ flowsAt?: (root: string) => NonNullable<SessionEndDeps['flows']>;
90
+ /** Who is driving — stamped on each saved flow. Injected: teardown does not reach into MCP. */
91
+ author?: () => DriveProgram['author'];
84
92
  /**
85
93
  * Called when a run artifact is written, so cloud sync can cycle instead of waiting for its timer.
86
94
  *
@@ -1,4 +1,11 @@
1
- import { driveFlowsFrom } from './drive-flow.js';
1
+ import { DRIVE_FLOW_PREFIX, driveFlowsFrom, } from './drive-flow.js';
2
+ import { log } from '../../log.js';
3
+ /** What a session's teardown writes to the daemon log when a by-product fails to save. */
4
+ const SESSION_END_LOG = {
5
+ FLOW_SAVE_FAILED: 'reticle_drive_flow_save_failed',
6
+ RUN_RECORD_FAILED: 'reticle_drive_run_record_failed',
7
+ };
8
+ const errorMessage = (error) => error instanceof Error ? error.message : String(error);
2
9
  import { journalActionsPath, sessionDirPath } from '../project/dir/reticle-dir.js';
3
10
  import { asSessionId } from '@reticlehq/core';
4
11
  import { OnboardingPhase, OnboardingStepStatus, } from '@reticlehq/core/telemetry';
@@ -78,24 +85,27 @@ export function makeSessionEnd(deps) {
78
85
  // would buy under a minute of latency for a mutable handle held across two wiring sites.
79
86
  // A DRIVE IS A REGRESSION TEST, and it used to need somebody to remember to say so.
80
87
  //
81
- // `record{start}` + `flow_save` is the agent-facing route and agents do not take it: the corpus
82
- // measures 3 of 33 flows mutation-testable and 6 of 112 steps declaring a consequence, while the
83
- // engine catches 84 of the 86 bugs it structurally can. The ceiling is how many flows EXIST that
84
- // could go red, so a drive that is never saved is a regression test paid for and thrown away.
88
+ // `record{start}` + `flow_save` is the agent-facing route and agents do not take it: few saved
89
+ // flows are mutation-testable and few steps declare a consequence, while the engine catches
90
+ // nearly every bug it structurally can. The ceiling is how many flows EXIST that could go red,
91
+ // so a drive that is never saved is a regression test paid for and thrown away.
85
92
  //
86
93
  // Saved only when the tape declared a consequence — see drive-flow.ts for why the alternative is
87
94
  // a false-green factory running once per session.
88
95
  try {
89
96
  await saveDrivenFlow(deps, session);
90
97
  }
91
- catch {
92
- // a flow is a by-product of the session; failing to write one never fails the teardown
98
+ catch (error) {
99
+ // A flow is a by-product of the session; failing to write one never fails the teardown. It is
100
+ // logged, because a drive that leaves no flow and no reason is otherwise undiagnosable.
101
+ log(SESSION_END_LOG.FLOW_SAVE_FAILED, { error: errorMessage(error) });
93
102
  }
94
103
  try {
95
104
  await recordDriveRun(deps, session);
96
105
  }
97
- catch {
98
- // an artifact is a report ABOUT the session; failing to write one never fails the teardown
106
+ catch (error) {
107
+ // An artifact is a report ABOUT the session; failing to write one never fails the teardown.
108
+ log(SESSION_END_LOG.RUN_RECORD_FAILED, { error: errorMessage(error) });
99
109
  }
100
110
  try {
101
111
  // Bound the journal on disk HERE, not only at daemon start.
@@ -196,13 +206,15 @@ export async function recordDriveRun(deps, session) {
196
206
  /** Persist what this session drove as a replayable flow, when it declared anything provable. */
197
207
  async function saveDrivenFlow(deps, session) {
198
208
  const takeTape = deps.takeAmbientTape;
199
- const flows = deps.flows;
209
+ const flows = deps.flowsAt?.(session.artifactRoot ?? deps.reticleRoot) ?? deps.flows;
200
210
  if (takeTape === undefined || flows === undefined)
201
211
  return;
202
212
  // One flow per journey the session contained, not one flow per session — see drive-flow.ts.
203
- const { programs, outcome } = driveFlowsFrom(session.id, takeTape());
213
+ const { programs, outcome } = driveFlowsFrom(takeTape());
214
+ const author = deps.author?.();
204
215
  for (const program of programs) {
205
- await flows.save(program, undefined, session.projectId);
216
+ // Folded into any saved copy of the same journey, so each session does not add a near-copy.
217
+ await flows.saveJourney(author === undefined ? program : { ...program, author }, session.projectId, DRIVE_FLOW_PREFIX);
206
218
  }
207
219
  // The funnel step that makes the SECOND run cheap.
208
220
  //
@@ -33,6 +33,8 @@ export interface ReticleDirPaths {
33
33
  capsules: string;
34
34
  /**.../.reticle/flake.json (per-flow flake ledger) */
35
35
  flake: string;
36
+ /**.../.reticle/coverage.json (app-wide coverage ledger, all six levels) */
37
+ coverage: string;
36
38
  /**.../.reticle/assertion-tiers.json (last-passing assertion tiers; anti-reward-hacking baseline) */
37
39
  tiers: string;
38
40
  }
@@ -18,6 +18,7 @@ export function reticleDirPaths(root) {
18
18
  ambient: join(root, ReticleDir.AMBIENT_FILE),
19
19
  capsules: join(root, ReticleDir.CAPSULES_SUBDIR),
20
20
  flake: join(root, ReticleDir.FLAKE_FILE),
21
+ coverage: join(root, ReticleDir.COVERAGE_FILE),
21
22
  tiers: join(root, ReticleDir.TIERS_FILE),
22
23
  };
23
24
  }
@@ -38,7 +38,9 @@ function numericDelta(before, after) {
38
38
  * "what's broken vs before" from the cloud. Absent creds / unreachable → undefined (agent stays local).
39
39
  */
40
40
  async function cloudRegression(deps, sessionId) {
41
- const config = resolveCloudConfig(process.env);
41
+ // The same resolver every caller uses (stored key for a linked repo, else the exported one);
42
+ // the environment alone only for an embedder that wires no linked-credential port.
43
+ const config = await (deps.linkedCloud?.() ?? Promise.resolve(resolveCloudConfig(process.env)));
42
44
  if (null === config)
43
45
  return undefined;
44
46
  let projectId;
@@ -4,7 +4,7 @@
4
4
  * A daemon left running by one project answers the next project's app and rejects it on token, and
5
5
  * the SDK printed `bridge refused the connection: authentication failed`. That sends someone to
6
6
  * check a token which is perfectly correct — it is simply a DIFFERENT project's token. The two cases
7
- * need opposite fixes: "your token is wrong" versus "that daemon is not yours, stop it".
7
+ * need opposite fixes: "your token is wrong" versus "that daemon is not yours, use your own port".
8
8
  *
9
9
  * The discriminator is EVIDENCE, not derivation. The daemon and the SDK derive project ids by
10
10
  * different schemes (the SDK from its Vite root, the daemon from its own cwd), so comparing those
@@ -4,7 +4,7 @@
4
4
  * A daemon left running by one project answers the next project's app and rejects it on token, and
5
5
  * the SDK printed `bridge refused the connection: authentication failed`. That sends someone to
6
6
  * check a token which is perfectly correct — it is simply a DIFFERENT project's token. The two cases
7
- * need opposite fixes: "your token is wrong" versus "that daemon is not yours, stop it".
7
+ * need opposite fixes: "your token is wrong" versus "that daemon is not yours, use your own port".
8
8
  *
9
9
  * The discriminator is EVIDENCE, not derivation. The daemon and the SDK derive project ids by
10
10
  * different schemes (the SDK from its Vite root, the daemon from its own cwd), so comparing those
@@ -62,7 +62,9 @@ export function authFailureReason(servedProjects, helloProject, helloToken) {
62
62
  if (helloProject !== undefined &&
63
63
  1 === servedProjects.size &&
64
64
  !servedProjects.has(helloProject)) {
65
- const reason = `${DIFFERENT_PROJECT_PREFIX} — run \`reticle stop\` and retry`;
65
+ // Not "stop it": that daemon is serving somebody else's app. This project needs its own port,
66
+ // which the CLI and the build plugins both read from `.reticle.json`.
67
+ const reason = `${DIFFERENT_PROJECT_PREFIX} — set a free "port" in .reticle.json, then restart the dev server`;
66
68
  return Buffer.byteLength(reason, 'utf8') <= MAX_REASON_BYTES ? reason : PLAIN;
67
69
  }
68
70
  if (helloToken === undefined || 0 === helloToken.length)
@@ -38,6 +38,14 @@ export declare const WS_CLOSE_REASON: {
38
38
  * refusal on this path: nothing about the app is wrong and retrying is the correct response.
39
39
  */
40
40
  readonly HANDSHAKE_POOL_FULL: string;
41
+ /**
42
+ * A page's HELLO failed the wire schema for a reason other than its protocol number — the SDK and
43
+ * this daemon are different releases whose contracts no longer agree. The socket is still closed
44
+ * with the short `invalid message` the SDK already prints; this is the sentence the no-session
45
+ * diagnosis records, because "never dialled the bridge" was what `init` said about a page this
46
+ * daemon had just turned away. Same `cause — fix` shape as the protocol-skew reasons.
47
+ */
48
+ readonly INVALID_HELLO: string;
41
49
  };
42
50
  interface BridgeOptions {
43
51
  port: number;
@@ -69,6 +69,16 @@ export const WS_CLOSE_REASON = {
69
69
  'this dial never got to identify itself. Nothing is wrong with the app; the pool drains on its ' +
70
70
  'own and reloading the page reconnects. If it persists, something else on this machine is ' +
71
71
  'opening sockets to this port.',
72
+ /**
73
+ * A page's HELLO failed the wire schema for a reason other than its protocol number — the SDK and
74
+ * this daemon are different releases whose contracts no longer agree. The socket is still closed
75
+ * with the short `invalid message` the SDK already prints; this is the sentence the no-session
76
+ * diagnosis records, because "never dialled the bridge" was what `init` said about a page this
77
+ * daemon had just turned away. Same `cause — fix` shape as the protocol-skew reasons.
78
+ */
79
+ INVALID_HELLO: "invalid message: the page's hello does not match this daemon's wire contract — put " +
80
+ '@reticlehq/browser, the build plugin and @reticlehq/server on one release, run `reticle stop`, ' +
81
+ 'then restart the dev server',
72
82
  };
73
83
  /**
74
84
  * A dial turned away at the ORIGIN gate, in words the reader can act on.
@@ -179,6 +189,25 @@ function helloProtocolMismatch(text) {
179
189
  }
180
190
  return null;
181
191
  }
192
+ /**
193
+ * What a HELLO says about the page it came from, read loosely enough to survive the schema failing.
194
+ * Null when the message is not a hello at all — only an SDK sends one, so only then is a refusal
195
+ * evidence about an app.
196
+ */
197
+ function peekHelloPage(text) {
198
+ try {
199
+ const json = JSON.parse(text);
200
+ if (json.kind !== MessageKind.HELLO)
201
+ return null;
202
+ return {
203
+ ...('string' === typeof json.url ? { url: json.url } : {}),
204
+ ...('string' === typeof json.projectId ? { projectId: json.projectId } : {}),
205
+ };
206
+ }
207
+ catch {
208
+ return null;
209
+ }
210
+ }
182
211
  /** Normalize ws RawData (string | Buffer | Buffer[] | ArrayBuffer) into a UTF-8 string. */
183
212
  function rawToString(raw) {
184
213
  if ('string' === typeof raw)
@@ -436,10 +465,19 @@ export class Bridge {
436
465
  // Told to the AGENT too, not only to the SDK that is about to stop retrying. Without this
437
466
  // the next tool call answers "no browser session connected", which reads as "no app is
438
467
  // running" — so an SDK too old to connect is invisible.
439
- this.sessions.noteClosure(reason, this.#clock());
468
+ this.sessions.noteClosure(reason, this.#clock(), peekHelloPage(text) ?? {});
440
469
  socket.close(WS_CLOSE.PROTOCOL_MISMATCH[0], reason);
441
470
  return;
442
471
  }
472
+ // Recorded only for a HELLO on a socket that has none yet: that is a page being turned
473
+ // away, and it used to leave no trace, so `init` reported "never dialled the bridge" about
474
+ // a page this daemon had just refused. A malformed frame on a live session is a different
475
+ // failure and says nothing about whether the app can connect.
476
+ const refused = undefined === session ? peekHelloPage(text) : null;
477
+ if (null !== refused) {
478
+ log('hello_invalid', { ...refused });
479
+ this.sessions.noteClosure(WS_CLOSE_REASON.INVALID_HELLO, this.#clock(), refused);
480
+ }
443
481
  socket.close(...WS_CLOSE.INVALID_MESSAGE);
444
482
  return;
445
483
  }
@@ -498,7 +536,10 @@ export class Bridge {
498
536
  presented: parsed.token !== undefined && 0 < parsed.token.length,
499
537
  tokenSource: this.#tokenSource,
500
538
  });
501
- this.sessions.noteClosure(reason, this.#clock());
539
+ this.sessions.noteClosure(reason, this.#clock(), {
540
+ url: parsed.url,
541
+ ...(parsed.projectId === undefined ? {} : { projectId: parsed.projectId }),
542
+ });
502
543
  socket.close(WS_CLOSE.AUTH_FAILED[0], reason);
503
544
  return;
504
545
  }
@@ -0,0 +1,25 @@
1
+ import type { Page } from 'playwright';
2
+ /**
3
+ * V8 JS coverage on a page Reticle drives — the raw half of "what of the app ever ran".
4
+ *
5
+ * Started once per page with `resetOnNavigation: false`, so a reload keeps counting instead of
6
+ * starting over. A take stops and immediately restarts: the counts it returns are since the last
7
+ * take, and the reader folds them as a union (see features/exhaust/code-coverage.ts).
8
+ *
9
+ * Chromium only. Anything else — or a page that closed — answers false/undefined rather than
10
+ * throwing: coverage is an addition to a drive, never a reason for one to fail.
11
+ */
12
+ /** One script's coverage — the shape `stopJSCoverage` returns, narrowed to what is read. */
13
+ export interface ScriptCoverage {
14
+ url: string;
15
+ functions: {
16
+ functionName: string;
17
+ ranges: {
18
+ startOffset: number;
19
+ endOffset: number;
20
+ count: number;
21
+ }[];
22
+ }[];
23
+ }
24
+ export declare function startJsCoverage(page: Page, covering: WeakSet<Page>): Promise<boolean>;
25
+ export declare function takeJsCoverage(page: Page, covering: WeakSet<Page>): Promise<ScriptCoverage[] | undefined>;